From 5f3bf7b2667a6f4e9310c85085e39b79cc1910fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Thu, 26 Jun 2025 16:59:15 +0200 Subject: [PATCH 01/68] WIP strucutred cbor --- .../kotlinx/serialization/cbor/CborDecoder.kt | 11 + .../kotlinx/serialization/cbor/CborElement.kt | 341 ++++++++++++++++++ .../kotlinx/serialization/cbor/CborEncoder.kt | 5 + .../cbor/internal/CborElementSerializers.kt | 295 +++++++++++++++ .../cbor/internal/CborTreeReader.kt | 127 +++++++ .../serialization/cbor/internal/Decoder.kt | 17 + .../serialization/cbor/internal/Encoder.kt | 5 +- .../serialization/cbor/CborElementTest.kt | 338 +++++++++++++++++ 8 files changed, 1138 insertions(+), 1 deletion(-) create mode 100644 formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt create mode 100644 formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt create mode 100644 formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt create mode 100644 formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborDecoder.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborDecoder.kt index 13a773f3fa..da6bde3c06 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborDecoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborDecoder.kt @@ -31,4 +31,15 @@ public interface CborDecoder : Decoder { * Exposes the current [Cbor] instance and all its configuration flags. Useful for low-level custom serializers. */ public val cbor: Cbor + + /** + * Decodes the next element in the current input as [CborElement]. + * The type of the decoded element depends on the current state of the input and, when received + * by [serializer][KSerializer] in its [KSerializer.serialize] method, the type of the token directly matches + * the [kind][SerialDescriptor.kind]. + * + * This method is allowed to invoke only as the part of the whole deserialization process of the class, + * calling this method after invoking [beginStructure] or any `decode*` method will lead to unspecified behaviour. + */ + public fun decodeCborElement(): CborElement } diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt new file mode 100644 index 0000000000..4dd04ad961 --- /dev/null +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -0,0 +1,341 @@ +/* + * Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("unused") + +package kotlinx.serialization.cbor + +import kotlinx.serialization.* +import kotlinx.serialization.cbor.internal.* + +/** + * Class representing single CBOR element. + * Can be [CborPrimitive], [CborMap] or [CborList]. + * + * [CborElement.toString] properly prints CBOR tree as a human-readable representation. + * Whole hierarchy is serializable, but only when used with [Cbor] as [CborElement] is purely CBOR-specific structure + * which has a meaningful schemaless semantics only for CBOR. + * + * The whole hierarchy is [serializable][Serializable] only by [Cbor] format. + */ +@Serializable(with = CborElementSerializer::class) +public sealed class CborElement + +/** + * Class representing CBOR primitive value. + * CBOR primitives include numbers, strings, booleans, byte arrays and special null value [CborNull]. + */ +@Serializable(with = CborPrimitiveSerializer::class) +public sealed class CborPrimitive : CborElement() { + /** + * Content of given element as string. For [CborNull], this method returns a "null" string. + * [CborPrimitive.contentOrNull] should be used for [CborNull] to get a `null`. + */ + public abstract val content: String + + public override fun toString(): String = content +} + +/** + * Sealed class representing CBOR number value. + * Can be either [Signed] or [Unsigned]. + */ +@Serializable(with = CborNumberSerializer::class) +public sealed class CborNumber : CborPrimitive() { + /** + * Returns the value as a [Byte]. + */ + public abstract val byte: Byte + + /** + * Returns the value as a [Short]. + */ + public abstract val short: Short + + /** + * Returns the value as an [Int]. + */ + public abstract val int: Int + + /** + * Returns the value as a [Long]. + */ + public abstract val long: Long + + /** + * Returns the value as a [Float]. + */ + public abstract val float: Float + + /** + * Returns the value as a [Double]. + */ + public abstract val double: Double + + /** + * Class representing a signed CBOR number value. + */ + public class Signed(@Contextual private val value: Number) : CborNumber() { + override val content: String get() = value.toString() + override val byte: Byte get() = value.toByte() + override val short: Short get() = value.toShort() + override val int: Int get() = value.toInt() + override val long: Long get() = value.toLong() + override val float: Float get() = value.toFloat() + override val double: Double get() = value.toDouble() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null) return false + + when (other) { + is Signed -> { + // Compare as double to handle different numeric types + return when { + // For integers, compare as long to avoid precision loss + value is Byte || value is Short || value is Int || value is Long || + other.value is Byte || other.value is Short || other.value is Int || other.value is Long -> { + value.toLong() == other.value.toLong() + } + // For floating point, compare as double + else -> { + value.toDouble() == other.value.toDouble() + } + } + } + is Unsigned -> { + // Only compare if both are non-negative integers + if (value is Byte || value is Short || value is Int || value is Long) { + val longValue = value.toLong() + return longValue >= 0 && longValue.toULong() == other.long.toULong() + } + return false + } + else -> return false + } + } + + override fun hashCode(): Int = value.hashCode() + } + + /** + * Class representing an unsigned CBOR number value. + */ + public class Unsigned(private val value: ULong) : CborNumber() { + override val content: String get() = value.toString() + override val byte: Byte get() = value.toByte() + override val short: Short get() = value.toShort() + override val int: Int get() = value.toInt() + override val long: Long get() = value.toLong() + override val float: Float get() = value.toFloat() + override val double: Double get() = value.toDouble() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null) return false + + when (other) { + is Unsigned -> { + return value == other.long.toULong() + } + is Signed -> { + // Only compare if the signed value is non-negative + val otherLong = other.long + return otherLong >= 0 && value == otherLong.toULong() + } + else -> return false + } + } + + override fun hashCode(): Int = value.hashCode() + } +} + +/** + * Class representing CBOR string value. + */ +@Serializable(with = CborStringSerializer::class) +public class CborString(private val value: String) : CborPrimitive() { + override val content: String get() = value + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + other as CborString + return value == other.value + } + + override fun hashCode(): Int = value.hashCode() +} + +/** + * Class representing CBOR boolean value. + */ +@Serializable(with = CborBooleanSerializer::class) +public class CborBoolean(private val value: Boolean) : CborPrimitive() { + override val content: String get() = value.toString() + + /** + * Returns the boolean value. + */ + public val boolean: Boolean get() = value + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + other as CborBoolean + return value == other.value + } + + override fun hashCode(): Int = value.hashCode() +} + +/** + * Class representing CBOR byte string value. + */ +@Serializable(with = CborByteStringSerializer::class) +public class CborByteString(private val value: ByteArray) : CborPrimitive() { + override val content: String get() = value.contentToString() + + /** + * Returns the byte array value. + */ + public val bytes: ByteArray get() = value.copyOf() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + other as CborByteString + return value.contentEquals(other.value) + } + + override fun hashCode(): Int = value.contentHashCode() +} + +/** + * Class representing CBOR `null` value + */ +@Serializable(with = CborNullSerializer::class) +public object CborNull : CborPrimitive() { + override val content: String = "null" +} + +/** + * Class representing CBOR map, consisting of key-value pairs, where both key and value are arbitrary [CborElement] + * + * Since this class also implements [Map] interface, you can use + * traditional methods like [Map.get] or [Map.getValue] to obtain CBOR elements. + */ +@Serializable(with = CborMapSerializer::class) +public class CborMap( + private val content: Map +) : CborElement(), Map by content { + public override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + other as CborMap + return content == other.content + } + public override fun hashCode(): Int = content.hashCode() + public override fun toString(): String { + return content.entries.joinToString( + separator = ", ", + prefix = "{", + postfix = "}", + transform = { (k, v) -> "$k: $v" } + ) + } +} + +/** + * Class representing CBOR array, consisting of indexed values, where value is arbitrary [CborElement] + * + * Since this class also implements [List] interface, you can use + * traditional methods like [List.get] or [List.getOrNull] to obtain CBOR elements. + */ +@Serializable(with = CborListSerializer::class) +public class CborList(private val content: List) : CborElement(), List by content { + public override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + other as CborList + return content == other.content + } + public override fun hashCode(): Int = content.hashCode() + public override fun toString(): String = content.joinToString(prefix = "[", postfix = "]", separator = ", ") +} + +/** + * Convenience method to get current element as [CborPrimitive] + * @throws IllegalArgumentException if current element is not a [CborPrimitive] + */ +public val CborElement.cborPrimitive: CborPrimitive + get() = this as? CborPrimitive ?: error("CborPrimitive") + +/** + * Convenience method to get current element as [CborMap] + * @throws IllegalArgumentException if current element is not a [CborMap] + */ +public val CborElement.cborMap: CborMap + get() = this as? CborMap ?: error("CborMap") + +/** + * Convenience method to get current element as [CborList] + * @throws IllegalArgumentException if current element is not a [CborList] + */ +public val CborElement.cborList: CborList + get() = this as? CborList ?: error("CborList") + +/** + * Convenience method to get current element as [CborNull] + * @throws IllegalArgumentException if current element is not a [CborNull] + */ +public val CborElement.cborNull: CborNull + get() = this as? CborNull ?: error("CborNull") + +/** + * Convenience method to get current element as [CborNumber] + * @throws IllegalArgumentException if current element is not a [CborNumber] + */ +public val CborElement.cborNumber: CborNumber + get() = this as? CborNumber ?: error("CborNumber") + +/** + * Convenience method to get current element as [CborString] + * @throws IllegalArgumentException if current element is not a [CborString] + */ +public val CborElement.cborString: CborString + get() = this as? CborString ?: error("CborString") + +/** + * Convenience method to get current element as [CborBoolean] + * @throws IllegalArgumentException if current element is not a [CborBoolean] + */ +public val CborElement.cborBoolean: CborBoolean + get() = this as? CborBoolean ?: error("CborBoolean") + +/** + * Convenience method to get current element as [CborByteString] + * @throws IllegalArgumentException if current element is not a [CborByteString] + */ +public val CborElement.cborByteString: CborByteString + get() = this as? CborByteString ?: error("CborByteString") + +/** + * Content of the given element as string or `null` if current element is [CborNull] + */ +public val CborPrimitive.contentOrNull: String? get() = if (this is CborNull) null else content + +/** + * Creates a [CborMap] from the given map entries. + */ +public fun CborMap(vararg pairs: Pair): CborMap = CborMap(mapOf(*pairs)) + +/** + * Creates a [CborList] from the given elements. + */ +public fun CborList(vararg elements: CborElement): CborList = CborList(listOf(*elements)) + +private fun CborElement.error(element: String): Nothing = + throw IllegalArgumentException("Element ${this::class} is not a $element") diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt index 7cfead426a..b7012fedb8 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt @@ -31,4 +31,9 @@ public interface CborEncoder : Encoder { * Exposes the current [Cbor] instance and all its configuration flags. Useful for low-level custom serializers. */ public val cbor: Cbor + + /** + * Encodes the specified [byteArray] as a CBOR byte string. + */ + public fun encodeByteArray(byteArray: ByteArray) } diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt new file mode 100644 index 0000000000..59bfa9428f --- /dev/null +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -0,0 +1,295 @@ +/* + * Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +@file:OptIn(ExperimentalSerializationApi::class, ExperimentalUnsignedTypes::class) + +package kotlinx.serialization.cbor.internal + +import kotlinx.serialization.* +import kotlinx.serialization.builtins.* +import kotlinx.serialization.cbor.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* +import kotlinx.serialization.modules.* + +/** + * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborElement]. + * It can only be used by with [Cbor] format and its input ([CborDecoder] and [CborEncoder]). + */ +internal object CborElementSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildSerialDescriptor("kotlinx.serialization.cbor.CborElement", PolymorphicKind.SEALED) { + // Resolve cyclic dependency in descriptors by late binding + element("CborPrimitive", defer { CborPrimitiveSerializer.descriptor }) + element("CborNull", defer { CborNullSerializer.descriptor }) + element("CborNumber", defer { CborNumberSerializer.descriptor }) + element("CborString", defer { CborStringSerializer.descriptor }) + element("CborBoolean", defer { CborBooleanSerializer.descriptor }) + element("CborByteString", defer { CborByteStringSerializer.descriptor }) + element("CborMap", defer { CborMapSerializer.descriptor }) + element("CborList", defer { CborListSerializer.descriptor }) + } + + override fun serialize(encoder: Encoder, value: CborElement) { + verify(encoder) + when (value) { + is CborPrimitive -> encoder.encodeSerializableValue(CborPrimitiveSerializer, value) + is CborMap -> encoder.encodeSerializableValue(CborMapSerializer, value) + is CborList -> encoder.encodeSerializableValue(CborListSerializer, value) + } + } + + override fun deserialize(decoder: Decoder): CborElement { + val input = decoder.asCborDecoder() + return input.decodeCborElement() + } +} + +/** + * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborPrimitive]. + * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). + */ +internal object CborPrimitiveSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildSerialDescriptor("kotlinx.serialization.cbor.CborPrimitive", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: CborPrimitive) { + verify(encoder) + when (value) { + is CborNull -> encoder.encodeSerializableValue(CborNullSerializer, CborNull) + is CborNumber -> encoder.encodeSerializableValue(CborNumberSerializer, value) + is CborString -> encoder.encodeSerializableValue(CborStringSerializer, value) + is CborBoolean -> encoder.encodeSerializableValue(CborBooleanSerializer, value) + is CborByteString -> encoder.encodeSerializableValue(CborByteStringSerializer, value) + } + } + + override fun deserialize(decoder: Decoder): CborPrimitive { + val result = decoder.asCborDecoder().decodeCborElement() + if (result !is CborPrimitive) throw CborDecodingException("Unexpected CBOR element, expected CborPrimitive, had ${result::class}") + return result + } +} + +/** + * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborNull]. + * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). + */ +internal object CborNullSerializer : KSerializer { + // technically, CborNull is an object, but it does not call beginStructure/endStructure at all + override val descriptor: SerialDescriptor = + buildSerialDescriptor("kotlinx.serialization.cbor.CborNull", SerialKind.ENUM) + + override fun serialize(encoder: Encoder, value: CborNull) { + verify(encoder) + encoder.encodeNull() + } + + override fun deserialize(decoder: Decoder): CborNull { + verify(decoder) + if (decoder.decodeNotNullMark()) { + throw CborDecodingException("Expected 'null' literal") + } + decoder.decodeNull() + return CborNull + } +} + +/** + * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborNumber]. + * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). + */ +internal object CborNumberSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborNumber", PrimitiveKind.DOUBLE) + + override fun serialize(encoder: Encoder, value: CborNumber) { + verify(encoder) + + when (value) { + is CborNumber.Unsigned -> { + // For unsigned numbers, we need to encode as a long + // The CBOR format will automatically use the correct encoding for unsigned numbers + encoder.encodeLong(value.long) + return + } + is CborNumber.Signed -> { + // For signed numbers, try to encode as the most specific type + try { + encoder.encodeLong(value.long) + return + } catch (e: Exception) { + // Not a valid long, try double + } + + try { + encoder.encodeDouble(value.double) + return + } catch (e: Exception) { + // Not a valid double, encode as string + } + + encoder.encodeString(value.content) + } + } + } + + override fun deserialize(decoder: Decoder): CborNumber { + val input = decoder.asCborDecoder() + val element = input.decodeCborElement() + if (element !is CborNumber) throw CborDecodingException("Unexpected CBOR element, expected CborNumber, had ${element::class}") + return element + } +} + +/** + * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborString]. + * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). + */ +internal object CborStringSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborString", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: CborString) { + verify(encoder) + encoder.encodeString(value.content) + } + + override fun deserialize(decoder: Decoder): CborString { + val input = decoder.asCborDecoder() + val element = input.decodeCborElement() + if (element !is CborString) throw CborDecodingException("Unexpected CBOR element, expected CborString, had ${element::class}") + return element + } +} + +/** + * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborBoolean]. + * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). + */ +internal object CborBooleanSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborBoolean", PrimitiveKind.BOOLEAN) + + override fun serialize(encoder: Encoder, value: CborBoolean) { + verify(encoder) + encoder.encodeBoolean(value.boolean) + } + + override fun deserialize(decoder: Decoder): CborBoolean { + val input = decoder.asCborDecoder() + val element = input.decodeCborElement() + if (element !is CborBoolean) throw CborDecodingException("Unexpected CBOR element, expected CborBoolean, had ${element::class}") + return element + } +} + +/** + * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborByteString]. + * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). + */ +internal object CborByteStringSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborByteString", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: CborByteString) { + verify(encoder) + val cborEncoder = encoder.asCborEncoder() + cborEncoder.encodeByteArray(value.bytes) + } + + override fun deserialize(decoder: Decoder): CborByteString { + val input = decoder.asCborDecoder() + val element = input.decodeCborElement() + if (element !is CborByteString) throw CborDecodingException("Unexpected CBOR element, expected CborByteString, had ${element::class}") + return element + } +} + +/** + * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborMap]. + * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). + */ +internal object CborMapSerializer : KSerializer { + private object CborMapDescriptor : SerialDescriptor by MapSerializer(CborElementSerializer, CborElementSerializer).descriptor { + @ExperimentalSerializationApi + override val serialName: String = "kotlinx.serialization.cbor.CborMap" + } + + override val descriptor: SerialDescriptor = CborMapDescriptor + + override fun serialize(encoder: Encoder, value: CborMap) { + verify(encoder) + MapSerializer(CborElementSerializer, CborElementSerializer).serialize(encoder, value) + } + + override fun deserialize(decoder: Decoder): CborMap { + verify(decoder) + return CborMap(MapSerializer(CborElementSerializer, CborElementSerializer).deserialize(decoder)) + } +} + +/** + * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborList]. + * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). + */ +internal object CborListSerializer : KSerializer { + private object CborListDescriptor : SerialDescriptor by ListSerializer(CborElementSerializer).descriptor { + @ExperimentalSerializationApi + override val serialName: String = "kotlinx.serialization.cbor.CborList" + } + + override val descriptor: SerialDescriptor = CborListDescriptor + + override fun serialize(encoder: Encoder, value: CborList) { + verify(encoder) + ListSerializer(CborElementSerializer).serialize(encoder, value) + } + + override fun deserialize(decoder: Decoder): CborList { + verify(decoder) + return CborList(ListSerializer(CborElementSerializer).deserialize(decoder)) + } +} + +private fun verify(encoder: Encoder) { + encoder.asCborEncoder() +} + +private fun verify(decoder: Decoder) { + decoder.asCborDecoder() +} + +internal fun Decoder.asCborDecoder(): CborDecoder = this as? CborDecoder + ?: throw IllegalStateException( + "This serializer can be used only with Cbor format." + + "Expected Decoder to be CborDecoder, got ${this::class}" + ) + +internal fun Encoder.asCborEncoder() = this as? CborEncoder + ?: throw IllegalStateException( + "This serializer can be used only with Cbor format." + + "Expected Encoder to be CborEncoder, got ${this::class}" + ) + +/** + * Returns serial descriptor that delegates all the calls to descriptor returned by [deferred] block. + * Used to resolve cyclic dependencies between recursive serializable structures. + */ +@OptIn(ExperimentalSerializationApi::class) +private fun defer(deferred: () -> SerialDescriptor): SerialDescriptor = object : SerialDescriptor { + private val original: SerialDescriptor by lazy(deferred) + + override val serialName: String + get() = original.serialName + override val kind: SerialKind + get() = original.kind + override val elementsCount: Int + get() = original.elementsCount + + override fun getElementName(index: Int): String = original.getElementName(index) + override fun getElementIndex(name: String): Int = original.getElementIndex(name) + override fun getElementAnnotations(index: Int): List = original.getElementAnnotations(index) + override fun getElementDescriptor(index: Int): SerialDescriptor = original.getElementDescriptor(index) + override fun isElementOptional(index: Int): Boolean = original.isElementOptional(index) +} diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt new file mode 100644 index 0000000000..c7810f923e --- /dev/null +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +@file:OptIn(ExperimentalSerializationApi::class, ExperimentalUnsignedTypes::class) + +package kotlinx.serialization.cbor.internal + +import kotlinx.serialization.* +import kotlinx.serialization.cbor.* + +/** + * [CborTreeReader] reads CBOR data from [parser] and constructs a [CborElement] tree. + */ +internal class CborTreeReader( + private val configuration: CborConfiguration, + private val parser: CborParser +) { + /** + * Reads the next CBOR element from the parser. + */ + fun read(): CborElement { + if (parser.isNull()) { + parser.nextNull() + return CborNull + } + + // Try to read different types of CBOR elements + try { + return CborBoolean(parser.nextBoolean()) + } catch (e: CborDecodingException) { + // Not a boolean, continue + } + + try { + return readArray() + } catch (e: CborDecodingException) { + // Not an array, continue + } + + try { + return readMap() + } catch (e: CborDecodingException) { + // Not a map, continue + } + + try { + return CborByteString(parser.nextByteString()) + } catch (e: CborDecodingException) { + // Not a byte string, continue + } + + try { + return CborString(parser.nextString()) + } catch (e: CborDecodingException) { + // Not a string, continue + } + + try { + return CborNumber.Signed(parser.nextFloat()) + } catch (e: CborDecodingException) { + // Not a float, continue + } + + try { + return CborNumber.Signed(parser.nextDouble()) + } catch (e: CborDecodingException) { + // Not a double, continue + } + + try { + val (value, isSigned) = parser.nextNumberWithSign() + return if (isSigned) { + CborNumber.Signed(value) + } else { + CborNumber.Unsigned(value.toULong()) + } + } catch (e: CborDecodingException) { + // Not a number, continue + } + + throw CborDecodingException("Unable to decode CBOR element") + } + + private fun readArray(): CborList { + val size = parser.startArray() + val elements = mutableListOf() + + if (size >= 0) { + // Definite length array + repeat(size) { + elements.add(read()) + } + } else { + // Indefinite length array + while (!parser.isEnd()) { + elements.add(read()) + } + parser.end() + } + + return CborList(elements) + } + + private fun readMap(): CborMap { + val size = parser.startMap() + val elements = mutableMapOf() + + if (size >= 0) { + // Definite length map + repeat(size) { + val key = read() + val value = read() + elements[key] = value + } + } else { + // Indefinite length map + while (!parser.isEnd()) { + val key = read() + val value = read() + elements[key] = value + } + parser.end() + } + + return CborMap(elements) + } +} 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..a9054ff0d6 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -15,6 +15,10 @@ import kotlinx.serialization.modules.* internal open class CborReader(override val cbor: Cbor, protected val parser: CborParser) : AbstractDecoder(), CborDecoder { + override fun decodeCborElement(): CborElement { + return CborTreeReader(cbor.configuration, parser).read() + } + protected var size = -1 private set protected var finiteMode = false @@ -368,6 +372,19 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO return res } + /** + * Reads a number from the input and returns it along with a flag indicating whether it is encoded as a + * negative integer (major type 1). + */ + fun nextNumberWithSign(tags: ULongArray? = null): Pair { + processTags(tags) + val headerByte = peekCurByteOrFail() + val isNegative = (headerByte and MAJOR_TYPE_MASK) == HEADER_NEGATIVE.toInt() + val value = readNumber() + readByte() + return value to isNegative + } + // Reads a value encoded using rules for the major type 0 (a.k.a. unsigned integers) private inline fun readUnsignedIntegerIgnoringMajorType(valueDescriptionForError: () -> String): Long { val additionalInfo = peekCurByteOrFail() and ADDITIONAL_INFO_MASK 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..4504ecaafe 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -28,6 +28,10 @@ internal sealed class CborWriter( override val cbor: Cbor, protected val output: ByteArrayOutput, ) : AbstractEncoder(), CborEncoder { + + override fun encodeByteArray(byteArray: ByteArray) { + getDestination().encodeByteString(byteArray) + } protected var isClass = false protected var encodeByteArrayAsByteString = false @@ -329,4 +333,3 @@ private fun composeNegative(value: Long): ByteArray { data[0] = data[0] or HEADER_NEGATIVE return data } - diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt new file mode 100644 index 0000000000..5460059e96 --- /dev/null +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -0,0 +1,338 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.cbor + +import kotlinx.serialization.* +import kotlin.test.* + +class CborElementTest { + + private val cbor = Cbor {} + + /** + * Helper method to decode a hex string to a CborElement + */ + private fun decodeHexToCborElement(hexString: String): CborElement { + val bytes = HexConverter.parseHexBinary(hexString.uppercase()) + return cbor.decodeFromByteArray(bytes) + } + + @Test + fun testCborNull() { + val nullElement = CborNull + val nullBytes = cbor.encodeToByteArray(nullElement) + val decodedNull = cbor.decodeFromByteArray(nullBytes) + assertEquals(nullElement, decodedNull) + } + + @Test + fun testCborNumber() { + val numberElement = CborNumber.Signed(42) + val numberBytes = cbor.encodeToByteArray(numberElement) + val decodedNumber = cbor.decodeFromByteArray(numberBytes) + assertEquals(numberElement, decodedNumber) + assertEquals(42, (decodedNumber as CborNumber).int) + } + + @Test + fun testCborString() { + val stringElement = CborString("Hello, CBOR!") + val stringBytes = cbor.encodeToByteArray(stringElement) + val decodedString = cbor.decodeFromByteArray(stringBytes) + assertEquals(stringElement, decodedString) + assertEquals("Hello, CBOR!", (decodedString as CborString).content) + } + + @Test + fun testCborBoolean() { + val booleanElement = CborBoolean(true) + val booleanBytes = cbor.encodeToByteArray(booleanElement) + val decodedBoolean = cbor.decodeFromByteArray(booleanBytes) + assertEquals(booleanElement, decodedBoolean) + assertEquals(true, (decodedBoolean as CborBoolean).boolean) + } + + @Test + fun testCborByteString() { + val byteArray = byteArrayOf(1, 2, 3, 4, 5) + val byteStringElement = CborByteString(byteArray) + val byteStringBytes = cbor.encodeToByteArray(byteStringElement) + val decodedByteString = cbor.decodeFromByteArray(byteStringBytes) + assertEquals(byteStringElement, decodedByteString) + assertTrue((decodedByteString as CborByteString).bytes.contentEquals(byteArray)) + } + + @Test + fun testCborList() { + val listElement = CborList( + listOf( + CborNumber.Signed(1), + CborString("two"), + CborBoolean(true), + CborNull + ) + ) + val listBytes = cbor.encodeToByteArray(listElement) + val decodedList = cbor.decodeFromByteArray(listBytes) + + // Verify the type and size + assertTrue(decodedList is CborList) + val decodedCborList = decodedList as CborList + assertEquals(4, decodedCborList.size) + + // Verify individual elements + assertTrue(decodedCborList[0] is CborNumber) + assertEquals(1, (decodedCborList[0] as CborNumber).int) + + assertTrue(decodedCborList[1] is CborString) + assertEquals("two", (decodedCborList[1] as CborString).content) + + assertTrue(decodedCborList[2] is CborBoolean) + assertEquals(true, (decodedCborList[2] as CborBoolean).boolean) + + assertTrue(decodedCborList[3] is CborNull) + } + + @Test + fun testCborMap() { + val mapElement = CborMap( + mapOf( + CborString("key1") to CborNumber.Signed(42), + CborString("key2") to CborString("value"), + CborNumber.Signed(3) to CborBoolean(true), + CborNull to CborNull + ) + ) + val mapBytes = cbor.encodeToByteArray(mapElement) + val decodedMap = cbor.decodeFromByteArray(mapBytes) + + // Verify the type and size + assertTrue(decodedMap is CborMap) + val decodedCborMap = decodedMap as CborMap + assertEquals(4, decodedCborMap.size) + + // Verify individual entries + assertTrue(decodedCborMap.containsKey(CborString("key1"))) + val value1 = decodedCborMap[CborString("key1")] + assertTrue(value1 is CborNumber) + assertEquals(42, (value1 as CborNumber).int) + + assertTrue(decodedCborMap.containsKey(CborString("key2"))) + val value2 = decodedCborMap[CborString("key2")] + assertTrue(value2 is CborString) + assertEquals("value", (value2 as CborString).content) + + assertTrue(decodedCborMap.containsKey(CborNumber.Signed(3))) + val value3 = decodedCborMap[CborNumber.Signed(3)] + assertTrue(value3 is CborBoolean) + assertEquals(true, (value3 as CborBoolean).boolean) + + assertTrue(decodedCborMap.containsKey(CborNull)) + val value4 = decodedCborMap[CborNull] + assertTrue(value4 is CborNull) + } + + @Test + fun testComplexNestedStructure() { + // Create a complex nested structure with maps and lists + val complexElement = CborMap( + mapOf( + CborString("primitives") to CborList( + listOf( + CborNumber.Signed(123), + CborString("text"), + CborBoolean(false), + CborByteString(byteArrayOf(10, 20, 30)), + CborNull + ) + ), + CborString("nested") to CborMap( + mapOf( + CborString("inner") to CborList( + listOf( + CborNumber.Signed(1), + CborNumber.Signed(2) + ) + ), + CborString("empty") to CborList(emptyList()) + ) + ) + ) + ) + + val complexBytes = cbor.encodeToByteArray(complexElement) + val decodedComplex = cbor.decodeFromByteArray(complexBytes) + + // Verify the type + assertTrue(decodedComplex is CborMap) + val map = decodedComplex as CborMap + + // Verify the primitives list + assertTrue(map.containsKey(CborString("primitives"))) + val primitivesValue = map[CborString("primitives")] + assertTrue(primitivesValue is CborList) + val primitives = primitivesValue as CborList + + assertEquals(5, primitives.size) + + assertTrue(primitives[0] is CborNumber) + assertEquals(123, (primitives[0] as CborNumber).int) + + assertTrue(primitives[1] is CborString) + assertEquals("text", (primitives[1] as CborString).content) + + assertTrue(primitives[2] is CborBoolean) + assertEquals(false, (primitives[2] as CborBoolean).boolean) + + assertTrue(primitives[3] is CborByteString) + assertTrue((primitives[3] as CborByteString).bytes.contentEquals(byteArrayOf(10, 20, 30))) + + assertTrue(primitives[4] is CborNull) + + // Verify the nested map + assertTrue(map.containsKey(CborString("nested"))) + val nestedValue = map[CborString("nested")] + assertTrue(nestedValue is CborMap) + val nested = nestedValue as CborMap + + assertEquals(2, nested.size) + + // Verify the inner list + assertTrue(nested.containsKey(CborString("inner"))) + val innerValue = nested[CborString("inner")] + assertTrue(innerValue is CborList) + val inner = innerValue as CborList + + assertEquals(2, inner.size) + + assertTrue(inner[0] is CborNumber) + assertEquals(1, (inner[0] as CborNumber).int) + + assertTrue(inner[1] is CborNumber) + assertEquals(2, (inner[1] as CborNumber).int) + + // Verify the empty list + assertTrue(nested.containsKey(CborString("empty"))) + val emptyValue = nested[CborString("empty")] + assertTrue(emptyValue is CborList) + val empty = emptyValue as CborList + + assertEquals(0, empty.size) + } + + @Test + fun testDecodeIntegers() { + // Test data from CborParserTest.testParseIntegers + val element = decodeHexToCborElement("0C") as CborNumber + assertEquals(12, element.int) + + } + + @Test + fun testDecodeStrings() { + // Test data from CborParserTest.testParseStrings + val element = decodeHexToCborElement("6568656C6C6F") + assertTrue(element is CborString) + assertEquals("hello", element.content) + + val longStringElement = decodeHexToCborElement("7828737472696E672074686174206973206C6F6E676572207468616E2032332063686172616374657273") + assertTrue(longStringElement is CborString) + assertEquals("string that is longer than 23 characters", longStringElement.content) + } + + @Test + fun testDecodeFloatingPoint() { + // Test data from CborParserTest.testParseDoubles + val doubleElement = decodeHexToCborElement("fb7e37e43c8800759c") + assertTrue(doubleElement is CborNumber) + assertEquals(1e+300, doubleElement.double) + + val floatElement = decodeHexToCborElement("fa47c35000") + assertTrue(floatElement is CborNumber) + assertEquals(100000.0f, floatElement.float) + } + + @Test + fun testDecodeByteString() { + // Test data from CborParserTest.testRfc7049IndefiniteByteStringExample + val element = decodeHexToCborElement("5F44aabbccdd43eeff99FF") + assertTrue(element is CborByteString) + val byteString = element as CborByteString + val expectedBytes = HexConverter.parseHexBinary("aabbccddeeff99") + assertTrue(byteString.bytes.contentEquals(expectedBytes)) + } + + @Test + fun testDecodeArray() { + // Test data from CborParserTest.testSkipCollections + val element = decodeHexToCborElement("830118ff1a00010000") + assertTrue(element is CborList) + val list = element as CborList + assertEquals(3, list.size) + assertEquals(1, (list[0] as CborNumber).int) + assertEquals(255, (list[1] as CborNumber).int) + assertEquals(65536, (list[2] as CborNumber).int) + } + + @Test + fun testDecodeMap() { + // Test data from CborParserTest.testSkipCollections + val element = decodeHexToCborElement("a26178676b6f746c696e7861796d73657269616c697a6174696f6e") + assertTrue(element is CborMap) + val map = element as CborMap + assertEquals(2, map.size) + assertEquals(CborString("kotlinx"), map[CborString("x")]) + assertEquals(CborString("serialization"), map[CborString("y")]) + } + + @Test + fun testDecodeComplexStructure() { + // Test data from CborParserTest.testSkipIndefiniteLength + val element = decodeHexToCborElement("a461615f42cafe43010203ff61627f6648656c6c6f2065776f726c64ff61639f676b6f746c696e786d73657269616c697a6174696f6eff6164bf613101613202613303ff") + assertTrue(element is CborMap) + val map = element as CborMap + assertEquals(4, map.size) + + // Check the byte string + val byteString = map[CborString("a")] as CborByteString + val expectedBytes = HexConverter.parseHexBinary("cafe010203") + assertTrue(byteString.bytes.contentEquals(expectedBytes)) + + // Check the text string + assertEquals(CborString("Hello world"), map[CborString("b")]) + + // Check the array + val array = map[CborString("c")] as CborList + assertEquals(2, array.size) + assertEquals(CborString("kotlinx"), array[0]) + assertEquals(CborString("serialization"), array[1]) + + // Check the nested map + val nestedMap = map[CborString("d")] as CborMap + assertEquals(3, nestedMap.size) + assertEquals(CborNumber.Signed(1), nestedMap[CborString("1")]) + assertEquals(CborNumber.Signed(2), nestedMap[CborString("2")]) + assertEquals(CborNumber.Signed(3), nestedMap[CborString("3")]) + } + + @Test + fun testDecodeWithTags() { + // Test data from CborParserTest.testSkipTags + val element = decodeHexToCborElement("A46161CC1BFFFFFFFFFFFFFFFFD822616220D8386163D84E42CAFE6164D85ACC6B48656C6C6F20776F726C64") + assertTrue(element is CborMap) + val map = element as CborMap + assertEquals(4, map.size) + + // The tags are not preserved in the CborElement structure, but the values should be correct + assertEquals(CborNumber.Signed(Long.MAX_VALUE), map[CborString("a")]) + assertEquals(CborNumber.Signed(-1), map[CborString("b")]) + + val byteString = map[CborString("c")] as CborByteString + val expectedBytes = HexConverter.parseHexBinary("cafe") + assertTrue(byteString.bytes.contentEquals(expectedBytes)) + + assertEquals(CborString("Hello world"), map[CborString("d")]) + } +} From 7bf09d2bfd0fb8eaadded05130bbef13f10ac0a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 5 Jul 2025 02:07:03 +0200 Subject: [PATCH 02/68] proper when for decoding --- .../kotlinx/serialization/cbor/CborElement.kt | 186 ++++++------------ .../cbor/internal/CborElementSerializers.kt | 99 +++++----- .../cbor/internal/CborTreeReader.kt | 100 +++++----- .../serialization/cbor/CborElementTest.kt | 146 +++++++------- 4 files changed, 225 insertions(+), 306 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt index 4dd04ad961..5175b1f282 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -28,136 +28,66 @@ public sealed class CborElement */ @Serializable(with = CborPrimitiveSerializer::class) public sealed class CborPrimitive : CborElement() { - /** - * Content of given element as string. For [CborNull], this method returns a "null" string. - * [CborPrimitive.contentOrNull] should be used for [CborNull] to get a `null`. - */ - public abstract val content: String - public override fun toString(): String = content } /** - * Sealed class representing CBOR number value. - * Can be either [Signed] or [Unsigned]. + * Class representing signed CBOR integer (major type 1). */ -@Serializable(with = CborNumberSerializer::class) -public sealed class CborNumber : CborPrimitive() { - /** - * Returns the value as a [Byte]. - */ - public abstract val byte: Byte +@Serializable(with = CborIntSerializer::class) +public class CborNegativeInt(public val value: Long) : CborPrimitive() { + init { + require(value < 0) { "Number must be negative: $value" } + } - /** - * Returns the value as a [Short]. - */ - public abstract val short: Short + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + other as CborNegativeInt + return value == other.value + } - /** - * Returns the value as an [Int]. - */ - public abstract val int: Int + override fun hashCode(): Int = value.hashCode() +} - /** - * Returns the value as a [Long]. - */ - public abstract val long: Long +/** + * Class representing unsigned CBOR integer (major type 0). + */ +@Serializable(with = CborUIntSerializer::class) +public class CborPositiveInt(public val value: ULong) : CborPrimitive() { - /** - * Returns the value as a [Float]. - */ - public abstract val float: Float + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + other as CborPositiveInt + return value == other.value + } - /** - * Returns the value as a [Double]. - */ - public abstract val double: Double + override fun hashCode(): Int = value.hashCode() +} - /** - * Class representing a signed CBOR number value. - */ - public class Signed(@Contextual private val value: Number) : CborNumber() { - override val content: String get() = value.toString() - override val byte: Byte get() = value.toByte() - override val short: Short get() = value.toShort() - override val int: Int get() = value.toInt() - override val long: Long get() = value.toLong() - override val float: Float get() = value.toFloat() - override val double: Double get() = value.toDouble() - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null) return false - - when (other) { - is Signed -> { - // Compare as double to handle different numeric types - return when { - // For integers, compare as long to avoid precision loss - value is Byte || value is Short || value is Int || value is Long || - other.value is Byte || other.value is Short || other.value is Int || other.value is Long -> { - value.toLong() == other.value.toLong() - } - // For floating point, compare as double - else -> { - value.toDouble() == other.value.toDouble() - } - } - } - is Unsigned -> { - // Only compare if both are non-negative integers - if (value is Byte || value is Short || value is Int || value is Long) { - val longValue = value.toLong() - return longValue >= 0 && longValue.toULong() == other.long.toULong() - } - return false - } - else -> return false - } - } - - override fun hashCode(): Int = value.hashCode() - } +/** + * Class representing CBOR floating point value (major type 7). + */ +@Serializable(with = CborDoubleSerializer::class) +public class CborDouble(public val value: Double) : CborPrimitive() { - /** - * Class representing an unsigned CBOR number value. - */ - public class Unsigned(private val value: ULong) : CborNumber() { - override val content: String get() = value.toString() - override val byte: Byte get() = value.toByte() - override val short: Short get() = value.toShort() - override val int: Int get() = value.toInt() - override val long: Long get() = value.toLong() - override val float: Float get() = value.toFloat() - override val double: Double get() = value.toDouble() - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null) return false - - when (other) { - is Unsigned -> { - return value == other.long.toULong() - } - is Signed -> { - // Only compare if the signed value is non-negative - val otherLong = other.long - return otherLong >= 0 && value == otherLong.toULong() - } - else -> return false - } - } - - override fun hashCode(): Int = value.hashCode() + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + other as CborDouble + return value == other.value } + + override fun hashCode(): Int = value.hashCode() } + /** * Class representing CBOR string value. */ @Serializable(with = CborStringSerializer::class) -public class CborString(private val value: String) : CborPrimitive() { - override val content: String get() = value +public class CborString(public val value: String) : CborPrimitive() { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -174,7 +104,6 @@ public class CborString(private val value: String) : CborPrimitive() { */ @Serializable(with = CborBooleanSerializer::class) public class CborBoolean(private val value: Boolean) : CborPrimitive() { - override val content: String get() = value.toString() /** * Returns the boolean value. @@ -196,7 +125,6 @@ public class CborBoolean(private val value: Boolean) : CborPrimitive() { */ @Serializable(with = CborByteStringSerializer::class) public class CborByteString(private val value: ByteArray) : CborPrimitive() { - override val content: String get() = value.contentToString() /** * Returns the byte array value. @@ -218,7 +146,6 @@ public class CborByteString(private val value: ByteArray) : CborPrimitive() { */ @Serializable(with = CborNullSerializer::class) public object CborNull : CborPrimitive() { - override val content: String = "null" } /** @@ -237,6 +164,7 @@ public class CborMap( other as CborMap return content == other.content } + public override fun hashCode(): Int = content.hashCode() public override fun toString(): String { return content.entries.joinToString( @@ -262,6 +190,7 @@ public class CborList(private val content: List) : CborElement(), L other as CborList return content == other.content } + public override fun hashCode(): Int = content.hashCode() public override fun toString(): String = content.joinToString(prefix = "[", postfix = "]", separator = ", ") } @@ -295,11 +224,25 @@ public val CborElement.cborNull: CborNull get() = this as? CborNull ?: error("CborNull") /** - * Convenience method to get current element as [CborNumber] - * @throws IllegalArgumentException if current element is not a [CborNumber] + * Convenience method to get current element as [CborNegativeInt] + * @throws IllegalArgumentException if current element is not a [CborNegativeInt] + */ +public val CborElement.cborNegativeInt: CborNegativeInt + get() = this as? CborNegativeInt ?: error("CborNegativeInt") + +/** + * Convenience method to get current element as [CborPositiveInt] + * @throws IllegalArgumentException if current element is not a [CborPositiveInt] + */ +public val CborElement.cborPositiveInt: CborPositiveInt + get() = this as? CborPositiveInt ?: error("CborPositiveInt") + +/** + * Convenience method to get current element as [CborDouble] + * @throws IllegalArgumentException if current element is not a [CborDouble] */ -public val CborElement.cborNumber: CborNumber - get() = this as? CborNumber ?: error("CborNumber") +public val CborElement.cborDouble: CborDouble + get() = this as? CborDouble ?: error("CborDouble") /** * Convenience method to get current element as [CborString] @@ -322,11 +265,6 @@ public val CborElement.cborBoolean: CborBoolean public val CborElement.cborByteString: CborByteString get() = this as? CborByteString ?: error("CborByteString") -/** - * Content of the given element as string or `null` if current element is [CborNull] - */ -public val CborPrimitive.contentOrNull: String? get() = if (this is CborNull) null else content - /** * Creates a [CborMap] from the given map entries. */ @@ -338,4 +276,4 @@ public fun CborMap(vararg pairs: Pair): CborMap = Cbor public fun CborList(vararg elements: CborElement): CborList = CborList(listOf(*elements)) private fun CborElement.error(element: String): Nothing = - throw IllegalArgumentException("Element ${this::class} is not a $element") + throw IllegalArgumentException("Element ${this::class} is not a $element") \ No newline at end of file diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index 59bfa9428f..e1ec61ca24 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -10,7 +10,6 @@ import kotlinx.serialization.builtins.* import kotlinx.serialization.cbor.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* -import kotlinx.serialization.modules.* /** * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborElement]. @@ -22,12 +21,14 @@ internal object CborElementSerializer : KSerializer { // Resolve cyclic dependency in descriptors by late binding element("CborPrimitive", defer { CborPrimitiveSerializer.descriptor }) element("CborNull", defer { CborNullSerializer.descriptor }) - element("CborNumber", defer { CborNumberSerializer.descriptor }) element("CborString", defer { CborStringSerializer.descriptor }) element("CborBoolean", defer { CborBooleanSerializer.descriptor }) element("CborByteString", defer { CborByteStringSerializer.descriptor }) element("CborMap", defer { CborMapSerializer.descriptor }) element("CborList", defer { CborListSerializer.descriptor }) + element("CborDouble", defer { CborDoubleSerializer.descriptor }) + element("CborInt", defer { CborIntSerializer.descriptor }) + element("CborUInt", defer { CborUIntSerializer.descriptor }) } override fun serialize(encoder: Encoder, value: CborElement) { @@ -57,10 +58,12 @@ internal object CborPrimitiveSerializer : KSerializer { verify(encoder) when (value) { is CborNull -> encoder.encodeSerializableValue(CborNullSerializer, CborNull) - is CborNumber -> encoder.encodeSerializableValue(CborNumberSerializer, value) is CborString -> encoder.encodeSerializableValue(CborStringSerializer, value) is CborBoolean -> encoder.encodeSerializableValue(CborBooleanSerializer, value) is CborByteString -> encoder.encodeSerializableValue(CborByteStringSerializer, value) + is CborDouble -> encoder.encodeSerializableValue(CborDoubleSerializer, value) + is CborNegativeInt -> encoder.encodeSerializableValue(CborIntSerializer, value) + is CborPositiveInt -> encoder.encodeSerializableValue(CborUIntSerializer, value) } } @@ -95,50 +98,39 @@ internal object CborNullSerializer : KSerializer { } } -/** - * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborNumber]. - * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). - */ -internal object CborNumberSerializer : KSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborNumber", PrimitiveKind.DOUBLE) +public object CborIntSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborInt", PrimitiveKind.LONG) - override fun serialize(encoder: Encoder, value: CborNumber) { - verify(encoder) + override fun serialize(encoder: Encoder, value: CborNegativeInt) { + encoder.encodeLong(value.value) + } - when (value) { - is CborNumber.Unsigned -> { - // For unsigned numbers, we need to encode as a long - // The CBOR format will automatically use the correct encoding for unsigned numbers - encoder.encodeLong(value.long) - return - } - is CborNumber.Signed -> { - // For signed numbers, try to encode as the most specific type - try { - encoder.encodeLong(value.long) - return - } catch (e: Exception) { - // Not a valid long, try double - } - - try { - encoder.encodeDouble(value.double) - return - } catch (e: Exception) { - // Not a valid double, encode as string - } - - encoder.encodeString(value.content) - } - } + override fun deserialize(decoder: Decoder): CborNegativeInt { + return CborNegativeInt( decoder.decodeLong()) } +} - override fun deserialize(decoder: Decoder): CborNumber { - val input = decoder.asCborDecoder() - val element = input.decodeCborElement() - if (element !is CborNumber) throw CborDecodingException("Unexpected CBOR element, expected CborNumber, had ${element::class}") - return element +public object CborUIntSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("CborUInt", PrimitiveKind.LONG) + + override fun serialize(encoder: Encoder, value: CborPositiveInt) { + encoder.encodeInline(descriptor).encodeSerializableValue(ULong.serializer(), value.value) + } + + override fun deserialize(decoder: Decoder): CborPositiveInt { + return CborPositiveInt(decoder.decodeInline(descriptor).decodeSerializableValue(ULong.serializer())) + } +} + +public object CborDoubleSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborDouble", PrimitiveKind.DOUBLE) + + override fun serialize(encoder: Encoder, value: CborDouble) { + encoder.encodeDouble(value.value) + } + + override fun deserialize(decoder: Decoder): CborDouble { + return CborDouble(decoder.decodeDouble()) } } @@ -146,13 +138,13 @@ internal object CborNumberSerializer : KSerializer { * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborString]. * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). */ -internal object CborStringSerializer : KSerializer { +public object CborStringSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborString", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: CborString) { verify(encoder) - encoder.encodeString(value.content) + encoder.encodeString(value.value) } override fun deserialize(decoder: Decoder): CborString { @@ -167,7 +159,7 @@ internal object CborStringSerializer : KSerializer { * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborBoolean]. * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). */ -internal object CborBooleanSerializer : KSerializer { +public object CborBooleanSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborBoolean", PrimitiveKind.BOOLEAN) @@ -188,7 +180,7 @@ internal object CborBooleanSerializer : KSerializer { * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborByteString]. * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). */ -internal object CborByteStringSerializer : KSerializer { +public object CborByteStringSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborByteString", PrimitiveKind.STRING) @@ -210,8 +202,9 @@ internal object CborByteStringSerializer : KSerializer { * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborMap]. * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). */ -internal object CborMapSerializer : KSerializer { - private object CborMapDescriptor : SerialDescriptor by MapSerializer(CborElementSerializer, CborElementSerializer).descriptor { +public object CborMapSerializer : KSerializer { + private object CborMapDescriptor : + SerialDescriptor by MapSerializer(CborElementSerializer, CborElementSerializer).descriptor { @ExperimentalSerializationApi override val serialName: String = "kotlinx.serialization.cbor.CborMap" } @@ -233,7 +226,7 @@ internal object CborMapSerializer : KSerializer { * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborList]. * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). */ -internal object CborListSerializer : KSerializer { +public object CborListSerializer : KSerializer { private object CborListDescriptor : SerialDescriptor by ListSerializer(CborElementSerializer).descriptor { @ExperimentalSerializationApi override val serialName: String = "kotlinx.serialization.cbor.CborList" @@ -263,13 +256,13 @@ private fun verify(decoder: Decoder) { internal fun Decoder.asCborDecoder(): CborDecoder = this as? CborDecoder ?: throw IllegalStateException( "This serializer can be used only with Cbor format." + - "Expected Decoder to be CborDecoder, got ${this::class}" + "Expected Decoder to be CborDecoder, got ${this::class}" ) internal fun Encoder.asCborEncoder() = this as? CborEncoder ?: throw IllegalStateException( "This serializer can be used only with Cbor format." + - "Expected Encoder to be CborEncoder, got ${this::class}" + "Expected Encoder to be CborEncoder, got ${this::class}" ) /** @@ -292,4 +285,4 @@ private fun defer(deferred: () -> SerialDescriptor): SerialDescriptor = object : override fun getElementAnnotations(index: Int): List = original.getElementAnnotations(index) override fun getElementDescriptor(index: Int): SerialDescriptor = original.getElementDescriptor(index) override fun isElementOptional(index: Int): Boolean = original.isElementOptional(index) -} +} \ No newline at end of file diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt index c7810f923e..31e2c832fb 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt @@ -19,66 +19,60 @@ internal class CborTreeReader( * Reads the next CBOR element from the parser. */ fun read(): CborElement { - if (parser.isNull()) { - parser.nextNull() - return CborNull - } - - // Try to read different types of CBOR elements - try { - return CborBoolean(parser.nextBoolean()) - } catch (e: CborDecodingException) { - // Not a boolean, continue - } - - try { - return readArray() - } catch (e: CborDecodingException) { - // Not an array, continue - } + when (parser.curByte shr 5) { // Get major type from the first 3 bits + 0 -> { // Major type 0: unsigned integer + val value = parser.nextNumber() + return CborPositiveInt(value.toULong()) + } - try { - return readMap() - } catch (e: CborDecodingException) { - // Not a map, continue - } + 1 -> { // Major type 1: negative integer + val value = parser.nextNumber() + return CborNegativeInt(value) + } - try { - return CborByteString(parser.nextByteString()) - } catch (e: CborDecodingException) { - // Not a byte string, continue - } + 2 -> { // Major type 2: byte string + return CborByteString(parser.nextByteString()) + } - try { - return CborString(parser.nextString()) - } catch (e: CborDecodingException) { - // Not a string, continue - } + 3 -> { // Major type 3: text string + return CborString(parser.nextString()) + } - try { - return CborNumber.Signed(parser.nextFloat()) - } catch (e: CborDecodingException) { - // Not a float, continue - } + 4 -> { // Major type 4: array + return readArray() + } - try { - return CborNumber.Signed(parser.nextDouble()) - } catch (e: CborDecodingException) { - // Not a double, continue - } + 5 -> { // Major type 5: map + return readMap() + } - try { - val (value, isSigned) = parser.nextNumberWithSign() - return if (isSigned) { - CborNumber.Signed(value) - } else { - CborNumber.Unsigned(value.toULong()) + 7 -> { // Major type 7: simple/float/break + when (parser.curByte) { + 0xF4 -> { + parser.readByte() // Advance parser position + return CborBoolean(false) + } + 0xF5 -> { + parser.readByte() // Advance parser position + return CborBoolean(true) + } + 0xF6, 0xF7 -> { + parser.nextNull() + return CborNull + } + NEXT_HALF, NEXT_FLOAT, NEXT_DOUBLE -> return CborDouble(parser.nextDouble()) // Half/Float32/Float64 + else -> throw CborDecodingException( + "Invalid simple value or float type: ${ + parser.curByte.toString( + 16 + ) + }" + ) + } } - } catch (e: CborDecodingException) { - // Not a number, continue - } - throw CborDecodingException("Unable to decode CBOR element") + else -> throw CborDecodingException("Invalid CBOR major type: ${parser.curByte shr 5}") + } } private fun readArray(): CborList { @@ -124,4 +118,4 @@ internal class CborTreeReader( return CborMap(elements) } -} +} \ No newline at end of file diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index 5460059e96..8b429b5601 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -29,11 +29,11 @@ class CborElementTest { @Test fun testCborNumber() { - val numberElement = CborNumber.Signed(42) + val numberElement = CborPositiveInt(42u) val numberBytes = cbor.encodeToByteArray(numberElement) val decodedNumber = cbor.decodeFromByteArray(numberBytes) assertEquals(numberElement, decodedNumber) - assertEquals(42, (decodedNumber as CborNumber).int) + assertEquals(42u, (decodedNumber as CborPositiveInt).value) } @Test @@ -42,7 +42,7 @@ class CborElementTest { val stringBytes = cbor.encodeToByteArray(stringElement) val decodedString = cbor.decodeFromByteArray(stringBytes) assertEquals(stringElement, decodedString) - assertEquals("Hello, CBOR!", (decodedString as CborString).content) + assertEquals("Hello, CBOR!", (decodedString as CborString).value) } @Test @@ -68,7 +68,7 @@ class CborElementTest { fun testCborList() { val listElement = CborList( listOf( - CborNumber.Signed(1), + CborPositiveInt(1u), CborString("two"), CborBoolean(true), CborNull @@ -79,29 +79,28 @@ class CborElementTest { // Verify the type and size assertTrue(decodedList is CborList) - val decodedCborList = decodedList as CborList - assertEquals(4, decodedCborList.size) + assertEquals(4, decodedList.size) // Verify individual elements - assertTrue(decodedCborList[0] is CborNumber) - assertEquals(1, (decodedCborList[0] as CborNumber).int) + assertTrue(decodedList[0] is CborPositiveInt) + assertEquals(1u, (decodedList[0] as CborPositiveInt).value) - assertTrue(decodedCborList[1] is CborString) - assertEquals("two", (decodedCborList[1] as CborString).content) + assertTrue(decodedList[1] is CborString) + assertEquals("two", (decodedList[1] as CborString).value) - assertTrue(decodedCborList[2] is CborBoolean) - assertEquals(true, (decodedCborList[2] as CborBoolean).boolean) + assertTrue(decodedList[2] is CborBoolean) + assertEquals(true, (decodedList[2] as CborBoolean).boolean) - assertTrue(decodedCborList[3] is CborNull) + assertTrue(decodedList[3] is CborNull) } @Test fun testCborMap() { val mapElement = CborMap( mapOf( - CborString("key1") to CborNumber.Signed(42), + CborString("key1") to CborPositiveInt(42u), CborString("key2") to CborString("value"), - CborNumber.Signed(3) to CborBoolean(true), + CborPositiveInt(3u) to CborBoolean(true), CborNull to CborNull ) ) @@ -110,27 +109,26 @@ class CborElementTest { // Verify the type and size assertTrue(decodedMap is CborMap) - val decodedCborMap = decodedMap as CborMap - assertEquals(4, decodedCborMap.size) + assertEquals(4, decodedMap.size) // Verify individual entries - assertTrue(decodedCborMap.containsKey(CborString("key1"))) - val value1 = decodedCborMap[CborString("key1")] - assertTrue(value1 is CborNumber) - assertEquals(42, (value1 as CborNumber).int) + assertTrue(decodedMap.containsKey(CborString("key1"))) + val value1 = decodedMap[CborString("key1")] + assertTrue(value1 is CborPositiveInt) + assertEquals(42u, (value1 as CborPositiveInt).value) - assertTrue(decodedCborMap.containsKey(CborString("key2"))) - val value2 = decodedCborMap[CborString("key2")] + assertTrue(decodedMap.containsKey(CborString("key2"))) + val value2 = decodedMap[CborString("key2")] assertTrue(value2 is CborString) - assertEquals("value", (value2 as CborString).content) + assertEquals("value", (value2 as CborString).value) - assertTrue(decodedCborMap.containsKey(CborNumber.Signed(3))) - val value3 = decodedCborMap[CborNumber.Signed(3)] + assertTrue(decodedMap.containsKey(CborPositiveInt(3u))) + val value3 = decodedMap[CborPositiveInt(3u)] assertTrue(value3 is CborBoolean) assertEquals(true, (value3 as CborBoolean).boolean) - assertTrue(decodedCborMap.containsKey(CborNull)) - val value4 = decodedCborMap[CborNull] + assertTrue(decodedMap.containsKey(CborNull)) + val value4 = decodedMap[CborNull] assertTrue(value4 is CborNull) } @@ -141,7 +139,7 @@ class CborElementTest { mapOf( CborString("primitives") to CborList( listOf( - CborNumber.Signed(123), + CborPositiveInt(123u), CborString("text"), CborBoolean(false), CborByteString(byteArrayOf(10, 20, 30)), @@ -152,8 +150,8 @@ class CborElementTest { mapOf( CborString("inner") to CborList( listOf( - CborNumber.Signed(1), - CborNumber.Signed(2) + CborPositiveInt(1u), + CborPositiveInt(2u) ) ), CborString("empty") to CborList(emptyList()) @@ -167,57 +165,53 @@ class CborElementTest { // Verify the type assertTrue(decodedComplex is CborMap) - val map = decodedComplex as CborMap // Verify the primitives list - assertTrue(map.containsKey(CborString("primitives"))) - val primitivesValue = map[CborString("primitives")] + assertTrue(decodedComplex.containsKey(CborString("primitives"))) + val primitivesValue = decodedComplex[CborString("primitives")] assertTrue(primitivesValue is CborList) - val primitives = primitivesValue as CborList - assertEquals(5, primitives.size) + assertEquals(5, primitivesValue.size) - assertTrue(primitives[0] is CborNumber) - assertEquals(123, (primitives[0] as CborNumber).int) + assertTrue(primitivesValue[0] is CborPositiveInt) + assertEquals(123u, (primitivesValue[0] as CborPositiveInt).value) - assertTrue(primitives[1] is CborString) - assertEquals("text", (primitives[1] as CborString).content) + assertTrue(primitivesValue[1] is CborString) + assertEquals("text", (primitivesValue[1] as CborString).value) - assertTrue(primitives[2] is CborBoolean) - assertEquals(false, (primitives[2] as CborBoolean).boolean) + assertTrue(primitivesValue[2] is CborBoolean) + assertEquals(false, (primitivesValue[2] as CborBoolean).boolean) - assertTrue(primitives[3] is CborByteString) - assertTrue((primitives[3] as CborByteString).bytes.contentEquals(byteArrayOf(10, 20, 30))) + assertTrue(primitivesValue[3] is CborByteString) + assertTrue((primitivesValue[3] as CborByteString).bytes.contentEquals(byteArrayOf(10, 20, 30))) - assertTrue(primitives[4] is CborNull) + assertTrue(primitivesValue[4] is CborNull) // Verify the nested map - assertTrue(map.containsKey(CborString("nested"))) - val nestedValue = map[CborString("nested")] + assertTrue(decodedComplex.containsKey(CborString("nested"))) + val nestedValue = decodedComplex[CborString("nested")] assertTrue(nestedValue is CborMap) - val nested = nestedValue as CborMap - assertEquals(2, nested.size) + assertEquals(2, nestedValue.size) // Verify the inner list - assertTrue(nested.containsKey(CborString("inner"))) - val innerValue = nested[CborString("inner")] + assertTrue(nestedValue.containsKey(CborString("inner"))) + val innerValue = nestedValue[CborString("inner")] assertTrue(innerValue is CborList) - val inner = innerValue as CborList - assertEquals(2, inner.size) + assertEquals(2, innerValue.size) - assertTrue(inner[0] is CborNumber) - assertEquals(1, (inner[0] as CborNumber).int) + assertTrue(innerValue[0] is CborPositiveInt) + assertEquals(1u, (innerValue[0] as CborPositiveInt).value) - assertTrue(inner[1] is CborNumber) - assertEquals(2, (inner[1] as CborNumber).int) + assertTrue(innerValue[1] is CborPositiveInt) + assertEquals(2u, (innerValue[1] as CborPositiveInt).value) // Verify the empty list - assertTrue(nested.containsKey(CborString("empty"))) - val emptyValue = nested[CborString("empty")] + assertTrue(nestedValue.containsKey(CborString("empty"))) + val emptyValue = nestedValue[CborString("empty")] assertTrue(emptyValue is CborList) - val empty = emptyValue as CborList + val empty = emptyValue assertEquals(0, empty.size) } @@ -225,8 +219,8 @@ class CborElementTest { @Test fun testDecodeIntegers() { // Test data from CborParserTest.testParseIntegers - val element = decodeHexToCborElement("0C") as CborNumber - assertEquals(12, element.int) + val element = decodeHexToCborElement("0C") as CborPositiveInt + assertEquals(12u, element.value) } @@ -235,23 +229,23 @@ class CborElementTest { // Test data from CborParserTest.testParseStrings val element = decodeHexToCborElement("6568656C6C6F") assertTrue(element is CborString) - assertEquals("hello", element.content) + assertEquals("hello", element.value) val longStringElement = decodeHexToCborElement("7828737472696E672074686174206973206C6F6E676572207468616E2032332063686172616374657273") assertTrue(longStringElement is CborString) - assertEquals("string that is longer than 23 characters", longStringElement.content) + assertEquals("string that is longer than 23 characters", longStringElement.value) } @Test fun testDecodeFloatingPoint() { // Test data from CborParserTest.testParseDoubles val doubleElement = decodeHexToCborElement("fb7e37e43c8800759c") - assertTrue(doubleElement is CborNumber) - assertEquals(1e+300, doubleElement.double) + assertTrue(doubleElement is CborDouble) + assertEquals(1e+300, doubleElement.value) val floatElement = decodeHexToCborElement("fa47c35000") - assertTrue(floatElement is CborNumber) - assertEquals(100000.0f, floatElement.float) + assertTrue(floatElement is CborDouble) + assertEquals(100000.0f, floatElement.value.toFloat()) } @Test @@ -271,9 +265,9 @@ class CborElementTest { assertTrue(element is CborList) val list = element as CborList assertEquals(3, list.size) - assertEquals(1, (list[0] as CborNumber).int) - assertEquals(255, (list[1] as CborNumber).int) - assertEquals(65536, (list[2] as CborNumber).int) + assertEquals(1u, list[0].cborPositiveInt.value) + assertEquals(255u, list[1].cborPositiveInt.value) + assertEquals(65536u, list[2].cborPositiveInt.value) } @Test @@ -312,9 +306,9 @@ class CborElementTest { // Check the nested map val nestedMap = map[CborString("d")] as CborMap assertEquals(3, nestedMap.size) - assertEquals(CborNumber.Signed(1), nestedMap[CborString("1")]) - assertEquals(CborNumber.Signed(2), nestedMap[CborString("2")]) - assertEquals(CborNumber.Signed(3), nestedMap[CborString("3")]) + assertEquals(CborPositiveInt(1u), nestedMap[CborString("1")]) + assertEquals(CborPositiveInt(2u), nestedMap[CborString("2")]) + assertEquals(CborPositiveInt(3u), nestedMap[CborString("3")]) } @Test @@ -326,8 +320,8 @@ class CborElementTest { assertEquals(4, map.size) // The tags are not preserved in the CborElement structure, but the values should be correct - assertEquals(CborNumber.Signed(Long.MAX_VALUE), map[CborString("a")]) - assertEquals(CborNumber.Signed(-1), map[CborString("b")]) + assertEquals(CborNegativeInt(Long.MAX_VALUE), map[CborString("a")]) + assertEquals(CborNegativeInt(-1), map[CborString("b")]) val byteString = map[CborString("c")] as CborByteString val expectedBytes = HexConverter.parseHexBinary("cafe") From 80a736a798aeebf9237460c99a52cb7580ed72da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 5 Jul 2025 04:09:54 +0200 Subject: [PATCH 03/68] baseline tagging --- .../src/kotlinx/serialization/cbor/Cbor.kt | 12 +- .../kotlinx/serialization/cbor/CborElement.kt | 259 ++++++--------- .../cbor/internal/CborElementSerializers.kt | 52 ++- .../cbor/internal/CborTreeReader.kt | 55 +++- .../serialization/cbor/internal/Decoder.kt | 297 ++++++------------ .../serialization/cbor/internal/Encoder.kt | 6 +- .../serialization/cbor/CborElementTest.kt | 60 ++-- 7 files changed, 305 insertions(+), 436 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt index d922044847..f7d3a5a4c1 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt @@ -34,12 +34,12 @@ public sealed class Cbor( CborConfiguration( encodeDefaults = false, ignoreUnknownKeys = false, - encodeKeyTags = false, - encodeValueTags = false, - encodeObjectTags = false, - verifyKeyTags = false, - verifyValueTags = false, - verifyObjectTags = false, + encodeKeyTags = true, + encodeValueTags = true, + encodeObjectTags = true, + verifyKeyTags = true, + verifyValueTags = true, + verifyObjectTags = true, useDefiniteLengthEncoding = false, preferCborLabelsOverNames = false, alwaysUseByteString = false diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt index 5175b1f282..f655f137fd 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -3,6 +3,7 @@ */ @file:Suppress("unused") +@file:OptIn(ExperimentalUnsignedTypes::class) package kotlinx.serialization.cbor @@ -20,132 +21,144 @@ import kotlinx.serialization.cbor.internal.* * The whole hierarchy is [serializable][Serializable] only by [Cbor] format. */ @Serializable(with = CborElementSerializer::class) -public sealed class CborElement +public sealed class CborElement( + /** + * CBOR tags associated with this element. + * Tags are optional semantic tagging of other major types (major type 6). + * See [RFC 8949 3.4. Tagging of Items](https://datatracker.ietf.org/doc/html/rfc8949#name-tagging-of-items). + */ + @OptIn(ExperimentalUnsignedTypes::class) + public val tags: ULongArray = ulongArrayOf() +) /** * Class representing CBOR primitive value. * CBOR primitives include numbers, strings, booleans, byte arrays and special null value [CborNull]. */ @Serializable(with = CborPrimitiveSerializer::class) -public sealed class CborPrimitive : CborElement() { - -} +public sealed class CborPrimitive( + tags: ULongArray = ulongArrayOf() +) : CborElement(tags) /** * Class representing signed CBOR integer (major type 1). */ @Serializable(with = CborIntSerializer::class) -public class CborNegativeInt(public val value: Long) : CborPrimitive() { +public class CborNegativeInt( + public val value: Long, + tags: ULongArray = ulongArrayOf() +) : CborPrimitive(tags) { init { require(value < 0) { "Number must be negative: $value" } } - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - other as CborNegativeInt - return value == other.value - } + override fun equals(other: Any?): Boolean = + other is CborNegativeInt && other.value == value && other.tags.contentEquals(tags) - override fun hashCode(): Int = value.hashCode() + override fun hashCode(): Int = value.hashCode() * 31 + tags.contentHashCode() } /** * Class representing unsigned CBOR integer (major type 0). */ @Serializable(with = CborUIntSerializer::class) -public class CborPositiveInt(public val value: ULong) : CborPrimitive() { +public class CborPositiveInt( + public val value: ULong, + tags: ULongArray = ulongArrayOf() +) : CborPrimitive(tags) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - other as CborPositiveInt - return value == other.value - } + override fun equals(other: Any?): Boolean = + other is CborPositiveInt && other.value == value && other.tags.contentEquals(tags) - override fun hashCode(): Int = value.hashCode() + override fun hashCode(): Int = value.hashCode() * 31 + tags.contentHashCode() } /** * Class representing CBOR floating point value (major type 7). */ @Serializable(with = CborDoubleSerializer::class) -public class CborDouble(public val value: Double) : CborPrimitive() { +public class CborDouble( + public val value: Double, + tags: ULongArray = ulongArrayOf() +) : CborPrimitive(tags) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - other as CborDouble - return value == other.value - } + override fun equals(other: Any?): Boolean = + other is CborDouble && other.value == value && other.tags.contentEquals(tags) - override fun hashCode(): Int = value.hashCode() + override fun hashCode(): Int = value.hashCode() * 31 + tags.contentHashCode() } - /** * Class representing CBOR string value. */ @Serializable(with = CborStringSerializer::class) -public class CborString(public val value: String) : CborPrimitive() { +public class CborString( + public val value: String, + tags: ULongArray = ulongArrayOf() +) : CborPrimitive(tags) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - other as CborString - return value == other.value - } + override fun equals(other: Any?): Boolean = + other is CborString && other.value == value && other.tags.contentEquals(tags) - override fun hashCode(): Int = value.hashCode() + override fun hashCode(): Int = value.hashCode() * 31 + tags.contentHashCode() } /** * Class representing CBOR boolean value. */ @Serializable(with = CborBooleanSerializer::class) -public class CborBoolean(private val value: Boolean) : CborPrimitive() { +public class CborBoolean( + private val value: Boolean, + tags: ULongArray = ulongArrayOf() +) : CborPrimitive(tags) { /** * Returns the boolean value. */ public val boolean: Boolean get() = value - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - other as CborBoolean - return value == other.value - } + override fun equals(other: Any?): Boolean = + other is CborBoolean && other.value == value && other.tags.contentEquals(tags) - override fun hashCode(): Int = value.hashCode() + override fun hashCode(): Int = value.hashCode() * 31 + tags.contentHashCode() } /** * Class representing CBOR byte string value. */ @Serializable(with = CborByteStringSerializer::class) -public class CborByteString(private val value: ByteArray) : CborPrimitive() { +public class CborByteString( + private val value: ByteArray, + tags: ULongArray = ulongArrayOf() +) : CborPrimitive(tags) { /** * Returns the byte array value. */ public val bytes: ByteArray get() = value.copyOf() - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - other as CborByteString - return value.contentEquals(other.value) - } + override fun equals(other: Any?): Boolean = + other is CborByteString && other.value.contentEquals(value) && other.tags.contentEquals(tags) - override fun hashCode(): Int = value.contentHashCode() + override fun hashCode(): Int = value.contentHashCode() * 31 + tags.contentHashCode() } /** * Class representing CBOR `null` value */ @Serializable(with = CborNullSerializer::class) -public object CborNull : CborPrimitive() { +public class CborNull(tags: ULongArray=ulongArrayOf()) : CborPrimitive(tags) { + // Note: CborNull is an object, so it cannot have constructor parameters for tags + // If tags are needed for null values, this would need to be changed to a class + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CborNull) return false + return true + } + + override fun hashCode(): Int { + return this::class.hashCode() + } } /** @@ -156,124 +169,34 @@ public object CborNull : CborPrimitive() { */ @Serializable(with = CborMapSerializer::class) public class CborMap( - private val content: Map -) : CborElement(), Map by content { - public override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - other as CborMap - return content == other.content - } - - public override fun hashCode(): Int = content.hashCode() - public override fun toString(): String { - return content.entries.joinToString( - separator = ", ", - prefix = "{", - postfix = "}", - transform = { (k, v) -> "$k: $v" } - ) - } + private val content: Map, + tags: ULongArray = ulongArrayOf() +) : CborElement(tags), Map by content { + + public override fun equals(other: Any?): Boolean = + other is CborMap && other.content == content && other.tags.contentEquals(tags) + + public override fun hashCode(): Int = content.hashCode() * 31 + tags.contentHashCode() + + public override fun toString(): String = content.toString() } /** - * Class representing CBOR array, consisting of indexed values, where value is arbitrary [CborElement] + * Class representing CBOR array, consisting of CBOR elements. * * Since this class also implements [List] interface, you can use - * traditional methods like [List.get] or [List.getOrNull] to obtain CBOR elements. + * traditional methods like [List.get] or [List.size] to obtain CBOR elements. */ @Serializable(with = CborListSerializer::class) -public class CborList(private val content: List) : CborElement(), List by content { - public override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - other as CborList - return content == other.content - } - - public override fun hashCode(): Int = content.hashCode() - public override fun toString(): String = content.joinToString(prefix = "[", postfix = "]", separator = ", ") -} - -/** - * Convenience method to get current element as [CborPrimitive] - * @throws IllegalArgumentException if current element is not a [CborPrimitive] - */ -public val CborElement.cborPrimitive: CborPrimitive - get() = this as? CborPrimitive ?: error("CborPrimitive") - -/** - * Convenience method to get current element as [CborMap] - * @throws IllegalArgumentException if current element is not a [CborMap] - */ -public val CborElement.cborMap: CborMap - get() = this as? CborMap ?: error("CborMap") - -/** - * Convenience method to get current element as [CborList] - * @throws IllegalArgumentException if current element is not a [CborList] - */ -public val CborElement.cborList: CborList - get() = this as? CborList ?: error("CborList") - -/** - * Convenience method to get current element as [CborNull] - * @throws IllegalArgumentException if current element is not a [CborNull] - */ -public val CborElement.cborNull: CborNull - get() = this as? CborNull ?: error("CborNull") - -/** - * Convenience method to get current element as [CborNegativeInt] - * @throws IllegalArgumentException if current element is not a [CborNegativeInt] - */ -public val CborElement.cborNegativeInt: CborNegativeInt - get() = this as? CborNegativeInt ?: error("CborNegativeInt") - -/** - * Convenience method to get current element as [CborPositiveInt] - * @throws IllegalArgumentException if current element is not a [CborPositiveInt] - */ -public val CborElement.cborPositiveInt: CborPositiveInt - get() = this as? CborPositiveInt ?: error("CborPositiveInt") - -/** - * Convenience method to get current element as [CborDouble] - * @throws IllegalArgumentException if current element is not a [CborDouble] - */ -public val CborElement.cborDouble: CborDouble - get() = this as? CborDouble ?: error("CborDouble") - -/** - * Convenience method to get current element as [CborString] - * @throws IllegalArgumentException if current element is not a [CborString] - */ -public val CborElement.cborString: CborString - get() = this as? CborString ?: error("CborString") - -/** - * Convenience method to get current element as [CborBoolean] - * @throws IllegalArgumentException if current element is not a [CborBoolean] - */ -public val CborElement.cborBoolean: CborBoolean - get() = this as? CborBoolean ?: error("CborBoolean") - -/** - * Convenience method to get current element as [CborByteString] - * @throws IllegalArgumentException if current element is not a [CborByteString] - */ -public val CborElement.cborByteString: CborByteString - get() = this as? CborByteString ?: error("CborByteString") - -/** - * Creates a [CborMap] from the given map entries. - */ -public fun CborMap(vararg pairs: Pair): CborMap = CborMap(mapOf(*pairs)) - -/** - * Creates a [CborList] from the given elements. - */ -public fun CborList(vararg elements: CborElement): CborList = CborList(listOf(*elements)) - -private fun CborElement.error(element: String): Nothing = - throw IllegalArgumentException("Element ${this::class} is not a $element") \ No newline at end of file +public class CborList( + private val content: List, + tags: ULongArray = ulongArrayOf() +) : CborElement(tags), List by content { + + public override fun equals(other: Any?): Boolean = + other is CborList && other.content == content && other.tags.contentEquals(tags) + + public override fun hashCode(): Int = content.hashCode() * 31 + tags.contentHashCode() + + public override fun toString(): String = content.toString() +} \ No newline at end of file diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index e1ec61ca24..f61d67a0b3 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -15,6 +15,16 @@ import kotlinx.serialization.encoding.* * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborElement]. * It can only be used by with [Cbor] format and its input ([CborDecoder] and [CborEncoder]). */ + +internal fun CborWriter.encodeTags(value: CborElement) { // Encode tags if present + if (value.tags.isNotEmpty()) { + for (tag in value.tags) { + encodeTag(tag) + } + } + +} + internal object CborElementSerializer : KSerializer { override val descriptor: SerialDescriptor = buildSerialDescriptor("kotlinx.serialization.cbor.CborElement", PolymorphicKind.SEALED) { @@ -33,6 +43,8 @@ internal object CborElementSerializer : KSerializer { override fun serialize(encoder: Encoder, value: CborElement) { verify(encoder) + + // Encode the value when (value) { is CborPrimitive -> encoder.encodeSerializableValue(CborPrimitiveSerializer, value) is CborMap -> encoder.encodeSerializableValue(CborMapSerializer, value) @@ -56,8 +68,13 @@ internal object CborPrimitiveSerializer : KSerializer { override fun serialize(encoder: Encoder, value: CborPrimitive) { verify(encoder) + val cborEncoder = encoder.asCborEncoder() + + + cborEncoder.encodeTags(value) + when (value) { - is CborNull -> encoder.encodeSerializableValue(CborNullSerializer, CborNull) + is CborNull -> encoder.encodeSerializableValue(CborNullSerializer, value) is CborString -> encoder.encodeSerializableValue(CborStringSerializer, value) is CborBoolean -> encoder.encodeSerializableValue(CborBooleanSerializer, value) is CborByteString -> encoder.encodeSerializableValue(CborByteStringSerializer, value) @@ -84,7 +101,7 @@ internal object CborNullSerializer : KSerializer { buildSerialDescriptor("kotlinx.serialization.cbor.CborNull", SerialKind.ENUM) override fun serialize(encoder: Encoder, value: CborNull) { - verify(encoder) + verify(encoder).encodeTags(value) encoder.encodeNull() } @@ -94,7 +111,7 @@ internal object CborNullSerializer : KSerializer { throw CborDecodingException("Expected 'null' literal") } decoder.decodeNull() - return CborNull + return CborNull() } } @@ -102,6 +119,7 @@ public object CborIntSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborInt", PrimitiveKind.LONG) override fun serialize(encoder: Encoder, value: CborNegativeInt) { + verify(encoder).encodeTags(value) encoder.encodeLong(value.value) } @@ -112,11 +130,12 @@ public object CborIntSerializer : KSerializer { public object CborUIntSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("CborUInt", PrimitiveKind.LONG) - + override fun serialize(encoder: Encoder, value: CborPositiveInt) { + verify(encoder).encodeTags(value) encoder.encodeInline(descriptor).encodeSerializableValue(ULong.serializer(), value.value) } - + override fun deserialize(decoder: Decoder): CborPositiveInt { return CborPositiveInt(decoder.decodeInline(descriptor).decodeSerializableValue(ULong.serializer())) } @@ -126,6 +145,7 @@ public object CborDoubleSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborDouble", PrimitiveKind.DOUBLE) override fun serialize(encoder: Encoder, value: CborDouble) { + verify(encoder).encodeTags(value) encoder.encodeDouble(value.value) } @@ -143,7 +163,7 @@ public object CborStringSerializer : KSerializer { PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborString", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: CborString) { - verify(encoder) + verify(encoder).encodeTags(value) encoder.encodeString(value.value) } @@ -164,7 +184,7 @@ public object CborBooleanSerializer : KSerializer { PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborBoolean", PrimitiveKind.BOOLEAN) override fun serialize(encoder: Encoder, value: CborBoolean) { - verify(encoder) + verify(encoder).encodeTags(value) encoder.encodeBoolean(value.boolean) } @@ -185,8 +205,8 @@ public object CborByteStringSerializer : KSerializer { PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborByteString", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: CborByteString) { - verify(encoder) - val cborEncoder = encoder.asCborEncoder() + val cborEncoder = verify(encoder) + cborEncoder.encodeTags(value) cborEncoder.encodeByteArray(value.bytes) } @@ -213,6 +233,10 @@ public object CborMapSerializer : KSerializer { override fun serialize(encoder: Encoder, value: CborMap) { verify(encoder) + val cborEncoder = encoder.asCborEncoder() + + cborEncoder.encodeTags(value) + MapSerializer(CborElementSerializer, CborElementSerializer).serialize(encoder, value) } @@ -236,6 +260,10 @@ public object CborListSerializer : KSerializer { override fun serialize(encoder: Encoder, value: CborList) { verify(encoder) + val cborEncoder = encoder.asCborEncoder() + + cborEncoder.encodeTags(value) + ListSerializer(CborElementSerializer).serialize(encoder, value) } @@ -245,9 +273,9 @@ public object CborListSerializer : KSerializer { } } -private fun verify(encoder: Encoder) { +private fun verify(encoder: Encoder) = encoder.asCborEncoder() -} + private fun verify(decoder: Decoder) { decoder.asCborDecoder() @@ -259,7 +287,7 @@ internal fun Decoder.asCborDecoder(): CborDecoder = this as? CborDecoder "Expected Decoder to be CborDecoder, got ${this::class}" ) -internal fun Encoder.asCborEncoder() = this as? CborEncoder +internal fun Encoder.asCborEncoder() = this as? CborWriter ?: throw IllegalStateException( "This serializer can be used only with Cbor format." + "Expected Encoder to be CborEncoder, got ${this::class}" diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt index 31e2c832fb..be724547e8 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt @@ -19,48 +19,52 @@ internal class CborTreeReader( * Reads the next CBOR element from the parser. */ fun read(): CborElement { - when (parser.curByte shr 5) { // Get major type from the first 3 bits + // Read any tags before the actual value + val tags = readTags() + + val result = when (parser.curByte shr 5) { // Get major type from the first 3 bits 0 -> { // Major type 0: unsigned integer val value = parser.nextNumber() - return CborPositiveInt(value.toULong()) + CborPositiveInt(value.toULong(), tags) } 1 -> { // Major type 1: negative integer val value = parser.nextNumber() - return CborNegativeInt(value) + CborNegativeInt(value, tags) } 2 -> { // Major type 2: byte string - return CborByteString(parser.nextByteString()) + CborByteString(parser.nextByteString(), tags) } 3 -> { // Major type 3: text string - return CborString(parser.nextString()) + CborString(parser.nextString(), tags) } 4 -> { // Major type 4: array - return readArray() + readArray(tags) } 5 -> { // Major type 5: map - return readMap() + readMap(tags) } 7 -> { // Major type 7: simple/float/break when (parser.curByte) { 0xF4 -> { parser.readByte() // Advance parser position - return CborBoolean(false) + CborBoolean(false, tags) } 0xF5 -> { parser.readByte() // Advance parser position - return CborBoolean(true) + CborBoolean(true, tags) } 0xF6, 0xF7 -> { parser.nextNull() - return CborNull + CborNull(tags) } - NEXT_HALF, NEXT_FLOAT, NEXT_DOUBLE -> return CborDouble(parser.nextDouble()) // Half/Float32/Float64 + // Half/Float32/Float64 + NEXT_HALF, NEXT_FLOAT, NEXT_DOUBLE -> CborDouble(parser.nextDouble(), tags) else -> throw CborDecodingException( "Invalid simple value or float type: ${ parser.curByte.toString( @@ -73,9 +77,28 @@ internal class CborTreeReader( else -> throw CborDecodingException("Invalid CBOR major type: ${parser.curByte shr 5}") } + return result + } + + /** + * Reads any tags preceding the current value. + * @return An array of tags, possibly empty + */ + @OptIn(ExperimentalUnsignedTypes::class) + private fun readTags(): ULongArray { + val tags = mutableListOf() + + // Read tags (major type 6) until we encounter a non-tag + while ((parser.curByte shr 5) == 6) { // Major type 6: tag + val tag = parser.nextTag() + tags.add(tag) + } + + return tags.toULongArray() } - private fun readArray(): CborList { + + private fun readArray(tags: ULongArray): CborList { val size = parser.startArray() val elements = mutableListOf() @@ -92,10 +115,10 @@ internal class CborTreeReader( parser.end() } - return CborList(elements) + return CborList(elements, tags) } - private fun readMap(): CborMap { + private fun readMap(tags: ULongArray): CborMap { val size = parser.startMap() val elements = mutableMapOf() @@ -116,6 +139,6 @@ internal class CborTreeReader( parser.end() } - return CborMap(elements) + return CborMap(elements, tags) } -} \ No newline at end of file +} 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 a9054ff0d6..46150e6f28 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -133,18 +133,10 @@ internal open class CborReader(override val cbor: Cbor, protected val parser: Cb override fun decodeBoolean() = parser.nextBoolean(tags) - override fun decodeByte() = parser.nextNumberWithinRange( - tags, Byte.MIN_VALUE.toLong(), Byte.MAX_VALUE.toLong(), "Byte" - ).toByte() - override fun decodeShort() = parser.nextNumberWithinRange( - tags, Short.MIN_VALUE.toLong(), Short.MAX_VALUE.toLong(), "Short" - ).toShort() - override fun decodeChar() = parser.nextNumberWithinRange( - tags, Char.MIN_VALUE.code.toLong(), Char.MAX_VALUE.code.toLong(), "Char" - ).toInt().toChar() - override fun decodeInt() = parser.nextNumberWithinRange( - tags, Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong(), "Int" - ).toInt() + override fun decodeByte() = parser.nextNumber(tags).toByte() + override fun decodeShort() = parser.nextNumber(tags).toShort() + override fun decodeChar() = parser.nextNumber(tags).toInt().toChar() + override fun decodeInt() = parser.nextNumber(tags).toInt() override fun decodeLong() = parser.nextNumber(tags) override fun decodeNull() = parser.nextNull(tags) @@ -164,51 +156,67 @@ internal open class CborReader(override val cbor: Cbor, protected val parser: Cb } internal class CborParser(private val input: ByteArrayInput, private val verifyObjectTags: Boolean) { - private var curByteOrEof: Int = -1 - - private fun peekCurByteOrFail(): Int { - if (curByteOrEof == -1) throw CborDecodingException("Unexpected end of encoded CBOR document") - return curByteOrEof - } + internal var curByte: Int = -1 init { readByte() } - @IgnorableReturnValue - private fun readByte(): Int { - curByteOrEof = input.read() - return curByteOrEof + internal fun readByte(): Int { + curByte = input.read() + return curByte } - fun isEof() = curByteOrEof == -1 + fun isEof() = curByte == -1 private fun skipByte(expected: Int) { - val byte = peekCurByteOrFail() - if (byte != expected) throw CborDecodingException("byte ${printByte(expected)}", byte) + if (curByte != expected) throw CborDecodingException("byte ${printByte(expected)}", curByte) readByte() } - fun isNull() = with(peekCurByteOrFail()) { this == NULL || this == EMPTY_MAP } + fun isNull() = (curByte == NULL || curByte == EMPTY_MAP) + + // Add this method to CborParser class + private fun readUnsignedValueFromAdditionalInfo(additionalInfo: Int): Long { + return when (additionalInfo) { + in 0..23 -> additionalInfo.toLong() + 24 -> { + val nextByte = readByte() + if (nextByte == -1) throw CborDecodingException("Unexpected EOF") + nextByte.toLong() and 0xFF + } + 25 -> input.readExact(2) + 26 -> input.readExact(4) + 27 -> input.readExact(8) + else -> throw CborDecodingException("Invalid additional info: $additionalInfo") + } + } + + fun nextTag(): ULong { + if ((curByte shr 5) != 6) { + throw CborDecodingException("Expected tag (major type 6), got major type ${curByte shr 5}") + } + + val additionalInfo = curByte and 0x1F + return readUnsignedValueFromAdditionalInfo(additionalInfo).toULong().also { skipByte(curByte) } + } fun nextNull(tags: ULongArray? = null): Nothing? { processTags(tags) - if (isNull()) { - /* val _ = */ readByte() - return null + if (curByte == NULL) { + skipByte(NULL) + } else if (curByte == EMPTY_MAP) { + skipByte(EMPTY_MAP) } - throw CborDecodingException( - "null value (${NULL.toHexString()}) or empty map (${EMPTY_MAP.toHexString()})", - peekCurByteOrFail() - ) + return null } fun nextBoolean(tags: ULongArray? = null): Boolean { processTags(tags) - val ans = when (val byte = peekCurByteOrFail()) { + val ans = when (curByte) { TRUE -> true FALSE -> false - else -> throw CborDecodingException("boolean value", byte) + else -> throw CborDecodingException("boolean value", curByte) } readByte() return ans @@ -225,48 +233,25 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO collectionType: String ): Int { processTags(tags) - val header = peekCurByteOrFail() - if (header == unboundedHeader) { + if (curByte == unboundedHeader) { skipByte(unboundedHeader) return -1 } - if ((header and MAJOR_TYPE_MASK) != boundedHeaderMask) { - if (boundedHeaderMask == HEADER_ARRAY && (header and MAJOR_TYPE_MASK) == HEADER_BYTE_STRING) { - throw CborDecodingException( - "Expected a start of array, " + - "but found ${printByte(header)}, which corresponds to the start of a byte string. " + - "Make sure you correctly set 'alwaysUseByteString' setting " + - "and/or 'kotlinx.serialization.cbor.ByteString' annotation." - ) - } - throw CborDecodingException("start of $collectionType", header) - } - val majorType = header and MAJOR_TYPE_MASK - val sizeLimit = if (majorType == HEADER_MAP) Int.MAX_VALUE / 2 else Int.MAX_VALUE - val size = readUnsignedIntegerIgnoringMajorType { "$collectionType length" } - .asSizedElementLength(majorType, sizeLimit) + if ((curByte and 0b111_00000) != boundedHeaderMask) + throw CborDecodingException("start of $collectionType", curByte) + val size = readNumber().toInt() readByte() return size } - fun isEnd() = peekCurByteOrFail() == BREAK + fun isEnd() = curByte == BREAK fun end() = skipByte(BREAK) fun nextByteString(tags: ULongArray? = null): ByteArray { processTags(tags) - val header = peekCurByteOrFail() - if ((header and MAJOR_TYPE_MASK) != HEADER_BYTE_STRING) { - if (header and MAJOR_TYPE_MASK == HEADER_ARRAY) { - throw CborDecodingException( - "Expected a start of a byte string, " + - "but found ${printByte(header)}, which corresponds to the start of an array. " + - "Make sure you correctly set 'alwaysUseByteString' setting " + - "and/or 'kotlinx.serialization.cbor.ByteString' annotation." - ) - } - throw CborDecodingException("start of byte string", header) - } + if ((curByte and 0b111_00000) != HEADER_BYTE_STRING) + throw CborDecodingException("start of byte string", curByte) val arr = readBytes() readByte() return arr @@ -277,34 +262,28 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO //used for reading the tag names and names of tagged keys (of maps, and serialized classes) private fun nextTaggedString(tags: ULongArray?): Pair { val collectedTags = processTags(tags) - val headerByte = peekCurByteOrFail() - if ((headerByte and MAJOR_TYPE_MASK) != HEADER_STRING) - throw CborDecodingException("start of string", headerByte) + if ((curByte and 0b111_00000) != HEADER_STRING) + throw CborDecodingException("start of string", curByte) val arr = readBytes() val ans = arr.decodeToString() readByte() return ans to collectedTags } - private fun readBytes(): ByteArray { - val headerByte = peekCurByteOrFail() - return if (headerByte and ADDITIONAL_INFO_MASK == ADDITIONAL_INFORMATION_INDEFINITE_LENGTH) { - val majorType = headerByte and MAJOR_TYPE_MASK + private fun readBytes(): ByteArray = + if (curByte and 0b000_11111 == ADDITIONAL_INFORMATION_INDEFINITE_LENGTH) { readByte() - readIndefiniteLengthStringChunks(majorType) + readIndefiniteLengthBytes() } else { - val majorType = headerByte and MAJOR_TYPE_MASK - val strLen = readUnsignedIntegerIgnoringMajorType { "length" }.asSizedElementLength(majorType) + val strLen = readNumber().toInt() input.readExactNBytes(strLen) } - } - @IgnorableReturnValue private fun processTags(tags: ULongArray?): ULongArray? { var index = 0 val collectedTags = mutableListOf() - while ((peekCurByteOrFail() and MAJOR_TYPE_MASK) == HEADER_TAG) { - val readTag = readUnsignedIntegerIgnoringMajorType { "tag" }.toULong() // This is the tag number + while ((curByte and 0b111_00000) == HEADER_TAG) { + val readTag = readNumber().toULong() // This is the tag number collectedTags += readTag // value tags and object tags are intermingled (keyTags are always separate) // so this check only holds if we verify both @@ -344,26 +323,18 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO */ fun nextTaggedStringOrNumber(): Triple { val collectedTags = processTags(null) - val majorType = peekCurByteOrFail() - if ((majorType and MAJOR_TYPE_MASK) == HEADER_STRING) { + if ((curByte and 0b111_00000) == HEADER_STRING) { val arr = readBytes() val ans = arr.decodeToString() readByte() return Triple(ans, null, collectedTags) } else { - val res = readUnsignedIntegerIgnoringMajorType { majorType.majorTypeName } + val res = readNumber() readByte() return Triple(null, res, collectedTags) } } - internal fun nextNumberWithinRange(tags: ULongArray?, from: Long, to: Long, type: String): Long { - val number = nextNumber(tags) - if (number !in from..to) { - throw CborDecodingException("Decoded number $number is not within the range for type $type ([$from..$to])") - } - return number - } fun nextNumber(tags: ULongArray? = null): Long { processTags(tags) @@ -372,47 +343,12 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO return res } - /** - * Reads a number from the input and returns it along with a flag indicating whether it is encoded as a - * negative integer (major type 1). - */ - fun nextNumberWithSign(tags: ULongArray? = null): Pair { - processTags(tags) - val headerByte = peekCurByteOrFail() - val isNegative = (headerByte and MAJOR_TYPE_MASK) == HEADER_NEGATIVE.toInt() - val value = readNumber() - readByte() - return value to isNegative - } - - // Reads a value encoded using rules for the major type 0 (a.k.a. unsigned integers) - private inline fun readUnsignedIntegerIgnoringMajorType(valueDescriptionForError: () -> String): Long { - val additionalInfo = peekCurByteOrFail() and ADDITIONAL_INFO_MASK - - if (additionalInfo <= 23) return additionalInfo.toLong() - val bytesToRead = when (additionalInfo) { - 24 -> 1 - 25 -> 2 - 26 -> 4 - 27 -> 8 - else /* > 27 */ -> throw CborDecodingException( - "Unexpected value encoding when reading ${valueDescriptionForError()}. " + - "Expected addition info value < 28, got $additionalInfo " + - "(decoded from ${printByte(peekCurByteOrFail())})" - ) - } - return input.readExact(bytesToRead) - } - private fun readNumber(): Long { - val headerByte = peekCurByteOrFail() - val majorType = headerByte and MAJOR_TYPE_MASK - if (majorType != HEADER_NEGATIVE.toInt() && majorType != HEADER_POSITIVE.toInt()) { - throw CborDecodingException("an unsigned or negative integer", headerByte) - } - val negative = majorType == HEADER_NEGATIVE.toInt() - val unsignedValue = readUnsignedIntegerIgnoringMajorType { majorType.majorTypeName } - return if (negative) -(unsignedValue + 1) else unsignedValue + val additionalInfo = curByte and 0b000_11111 + val negative = (curByte and 0b111_00000) == HEADER_NEGATIVE.toInt() + + val value = readUnsignedValueFromAdditionalInfo(additionalInfo) + return if (negative) -(value + 1) else value } private fun ByteArrayInput.readExact(bytes: Int): Long { @@ -424,25 +360,21 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO return result } - private fun ByteArrayInput.ensureEnoughBytes(bytesCount: Int) { + private fun ByteArrayInput.readExactNBytes(bytesCount: Int): ByteArray { if (bytesCount > availableBytes) { - throw CborDecodingException("Unexpected EOF, available $availableBytes bytes, requested: $bytesCount") + error("Unexpected EOF, available $availableBytes bytes, requested: $bytesCount") } - } - - private fun ByteArrayInput.readExactNBytes(bytesCount: Int): ByteArray { - ensureEnoughBytes(bytesCount) val array = ByteArray(bytesCount) - val _ = read(array, 0, bytesCount) + read(array, 0, bytesCount) return array } fun nextFloat(tags: ULongArray? = null): Float { processTags(tags) - val res = when (val headerByte = peekCurByteOrFail()) { + val res = when (curByte) { NEXT_FLOAT -> Float.fromBits(readInt()) NEXT_HALF -> floatFromHalfBits(readShort()) - else -> throw CborDecodingException("float header", headerByte) + else -> throw CborDecodingException("float header", curByte) } readByte() return res @@ -450,20 +382,19 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO fun nextDouble(tags: ULongArray? = null): Double { processTags(tags) - val res = when (val headerByte = peekCurByteOrFail()) { + val res = when (curByte) { NEXT_DOUBLE -> Double.fromBits(readLong()) NEXT_FLOAT -> Float.fromBits(readInt()).toDouble() NEXT_HALF -> floatFromHalfBits(readShort()).toDouble() - else -> throw CborDecodingException("double header", headerByte) + else -> throw CborDecodingException("double header", curByte) } readByte() return res } private fun readLong(): Long { - input.ensureEnoughBytes(Long.SIZE_BYTES) var result = 0L - repeat(Long.SIZE_BYTES) { + for (i in 0..7) { val byte = input.read() result = (result shl 8) or byte.toLong() } @@ -471,16 +402,14 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO } private fun readShort(): Short { - input.ensureEnoughBytes(Short.SIZE_BYTES) val highByte = input.read() val lowByte = input.read() return (highByte shl 8 or lowByte).toShort() } private fun readInt(): Int { - input.ensureEnoughBytes(Int.SIZE_BYTES) var result = 0 - repeat(Int.SIZE_BYTES) { + for (i in 0..3) { val byte = input.read() result = (result shl 8) or byte } @@ -505,17 +434,19 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO processTags(tags) do { + if (isEof()) throw CborDecodingException("Unexpected EOF while skipping element") + if (isIndefinite()) { lengthStack.add(LENGTH_STACK_INDEFINITE) } else if (isEnd()) { if (lengthStack.removeLastOrNull() != LENGTH_STACK_INDEFINITE) - throw CborDecodingException("next data item", peekCurByteOrFail()) + throw CborDecodingException("next data item", curByte) prune(lengthStack) } else { - val header = peekCurByteOrFail() and MAJOR_TYPE_MASK + val header = curByte and 0b111_00000 val length = elementLength() if (header == HEADER_TAG) { - val _ = readUnsignedIntegerIgnoringMajorType { "tag" } + readNumber() } else if (header == HEADER_ARRAY || header == HEADER_MAP) { if (length > 0) lengthStack.add(length) else prune(lengthStack) // empty map or array automatically completes @@ -549,15 +480,14 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO } /** - * Determines if [peekCurByteOrFail] represents an indefinite length CBOR item. + * Determines if [curByte] represents an indefinite length CBOR item. * - * Per [RFC 8949: 3.2. Indefinite Lengths for Some Major Types](https://tools.ietf.org/html/rfc8949#section-3.2): + * Per [RFC 7049: 2.2. Indefinite Lengths for Some Major Types](https://tools.ietf.org/html/rfc7049#section-2.2): * > Four CBOR items (arrays, maps, byte strings, and text strings) can be encoded with an indefinite length */ private fun isIndefinite(): Boolean { - val curByte = peekCurByteOrFail() - val majorType = curByte and MAJOR_TYPE_MASK - val value = curByte and ADDITIONAL_INFO_MASK + val majorType = curByte and 0b111_00000 + val value = curByte and 0b000_11111 return value == ADDITIONAL_INFORMATION_INDEFINITE_LENGTH && (majorType == HEADER_ARRAY || majorType == HEADER_MAP || @@ -565,7 +495,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO } /** - * Determines the length of the CBOR item represented by [peekCurByteOrFail]; length has specific meaning based on the type: + * Determines the length of the CBOR item represented by [curByte]; length has specific meaning based on the type: * * | Major type | Length represents number of... | * |---------------------|--------------------------------| @@ -578,17 +508,12 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO * | 6. tag | bytes | */ private fun elementLength(): Int { - val curByte = peekCurByteOrFail() - val majorType = curByte and MAJOR_TYPE_MASK - val additionalInformation = curByte and ADDITIONAL_INFO_MASK + val majorType = curByte and 0b111_00000 + val additionalInformation = curByte and 0b000_11111 return when (majorType) { - HEADER_BYTE_STRING, HEADER_STRING, HEADER_ARRAY - -> readUnsignedIntegerIgnoringMajorType { "${majorType.majorTypeName} length" } - .asSizedElementLength(majorType) - HEADER_MAP - -> readUnsignedIntegerIgnoringMajorType { "map length" } - .asSizedElementLength(majorType, Int.MAX_VALUE / 2) * 2 + HEADER_BYTE_STRING, HEADER_STRING, HEADER_ARRAY -> readNumber().toInt() + HEADER_MAP -> readNumber().toInt() * 2 else -> when (additionalInformation) { 24 -> 1 25 -> 2 @@ -600,56 +525,20 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO } /** - * Reads fixed-length chunks constituting indefinite-length byte sequences (either a text, or a byte-string). + * Indefinite-length byte sequences contain an unknown number of fixed-length byte sequences (chunks). * - * @param majorType a type of the enclosing indefinite-length sequence ([HEADER_STRING] or [HEADER_BYTE_STRING]) * @return [ByteArray] containing all of the concatenated bytes found in the buffer. */ - private fun readIndefiniteLengthStringChunks(majorType: Int): ByteArray { + private fun readIndefiniteLengthBytes(): ByteArray { val byteStrings = mutableListOf() do { - val header = peekCurByteOrFail() - if (header and MAJOR_TYPE_MASK != majorType) { - throw CborDecodingException( - "a header of a chunk with a major type bits matching $majorType", - header - ) - } - if (header and ADDITIONAL_INFO_MASK == ADDITIONAL_INFORMATION_INDEFINITE_LENGTH) { - throw CborDecodingException("a fixed-length chunk", header) - } - val length = readUnsignedIntegerIgnoringMajorType { "length of a fixed-length chunk" } - .asSizedElementLength(majorType) - byteStrings.add(input.readExactNBytes(length)) + byteStrings.add(readBytes()) readByte() } while (!isEnd()) return byteStrings.flatten() } - - private fun Long.asSizedElementLength(majorType: Int, sizeLimit: Int = Int.MAX_VALUE): Int { - if (this in 0L..sizeLimit.toLong()) return this.toInt() - - val typeName = majorType.majorTypeName - - if (this < 0) { - throw CborDecodingException("negative length value was decoded for $typeName: $this") - } - throw CborDecodingException("length for $typeName is too large: $this") - } } -private val Int.majorTypeName: String - get() = when (this and MAJOR_TYPE_MASK) { - HEADER_BYTE_STRING -> "byte string" - HEADER_STRING -> "string" - HEADER_ARRAY -> "array" - HEADER_MAP -> "map" - HEADER_TAG -> "tag" - HEADER_POSITIVE.toInt() -> "unsigned integer" - HEADER_NEGATIVE.toInt() -> "negative integer" - else -> "" - } - private fun Iterable.flatten(): ByteArray { val output = ByteArray(sumOf { it.size }) var position = 0 @@ -688,7 +577,7 @@ private val normalizeBaseBits = SINGLE_PRECISION_NORMALIZE_BASE.toBits() /* - * For details about half-precision floating-point numbers see https://tools.ietf.org/html/rfc8949#name-half-precision + * For details about half-precision floating-point numbers see https://tools.ietf.org/html/rfc7049#appendix-D */ private fun floatFromHalfBits(bits: Short): Float { val intBits = bits.toInt() @@ -747,4 +636,4 @@ private fun SerialDescriptor.getElementIndexOrThrow(name: String): Int { " You can enable 'CborBuilder.ignoreUnknownKeys' property to ignore unknown keys" ) return index -} +} \ No newline at end of file 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 4504ecaafe..dc9b7bad85 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -38,7 +38,7 @@ internal sealed class CborWriter( class Data(val bytes: ByteArrayOutput, var elementCount: Int) - protected abstract fun getDestination(): ByteArrayOutput + internal abstract fun getDestination(): ByteArrayOutput override val serializersModule: SerializersModule get() = cbor.serializersModule @@ -147,6 +147,8 @@ internal sealed class CborWriter( incrementChildren() // needed for definite len encoding, NOOP for indefinite length encoding return true } + + internal fun encodeTag(tag: ULong)= getDestination().encodeTag(tag) } @@ -238,7 +240,7 @@ private fun ByteArrayOutput.startMap(size: ULong) { composePositiveInline(size, HEADER_MAP) } -private fun ByteArrayOutput.encodeTag(tag: ULong) { +internal fun ByteArrayOutput.encodeTag(tag: ULong) { composePositiveInline(tag, HEADER_TAG) } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index 8b429b5601..75a5e16883 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -21,7 +21,7 @@ class CborElementTest { @Test fun testCborNull() { - val nullElement = CborNull + val nullElement = CborNull() val nullBytes = cbor.encodeToByteArray(nullElement) val decodedNull = cbor.decodeFromByteArray(nullBytes) assertEquals(nullElement, decodedNull) @@ -71,7 +71,7 @@ class CborElementTest { CborPositiveInt(1u), CborString("two"), CborBoolean(true), - CborNull + CborNull() ) ) val listBytes = cbor.encodeToByteArray(listElement) @@ -101,7 +101,7 @@ class CborElementTest { CborString("key1") to CborPositiveInt(42u), CborString("key2") to CborString("value"), CborPositiveInt(3u) to CborBoolean(true), - CborNull to CborNull + CborNull() to CborNull() ) ) val mapBytes = cbor.encodeToByteArray(mapElement) @@ -127,8 +127,8 @@ class CborElementTest { assertTrue(value3 is CborBoolean) assertEquals(true, (value3 as CborBoolean).boolean) - assertTrue(decodedMap.containsKey(CborNull)) - val value4 = decodedMap[CborNull] + assertTrue(decodedMap.containsKey(CborNull())) + val value4 = decodedMap[CborNull()] assertTrue(value4 is CborNull) } @@ -143,7 +143,7 @@ class CborElementTest { CborString("text"), CborBoolean(false), CborByteString(byteArrayOf(10, 20, 30)), - CborNull + CborNull() ) ), CborString("nested") to CborMap( @@ -219,7 +219,7 @@ class CborElementTest { @Test fun testDecodeIntegers() { // Test data from CborParserTest.testParseIntegers - val element = decodeHexToCborElement("0C") as CborPositiveInt + val element = decodeHexToCborElement("0C") as CborPositiveInt assertEquals(12u, element.value) } @@ -231,7 +231,8 @@ class CborElementTest { assertTrue(element is CborString) assertEquals("hello", element.value) - val longStringElement = decodeHexToCborElement("7828737472696E672074686174206973206C6F6E676572207468616E2032332063686172616374657273") + val longStringElement = + decodeHexToCborElement("7828737472696E672074686174206973206C6F6E676572207468616E2032332063686172616374657273") assertTrue(longStringElement is CborString) assertEquals("string that is longer than 23 characters", longStringElement.value) } @@ -265,9 +266,9 @@ class CborElementTest { assertTrue(element is CborList) val list = element as CborList assertEquals(3, list.size) - assertEquals(1u, list[0].cborPositiveInt.value) - assertEquals(255u, list[1].cborPositiveInt.value) - assertEquals(65536u, list[2].cborPositiveInt.value) + assertEquals(1u, (list[0] as CborPositiveInt).value) + assertEquals(255u, (list[1] as CborPositiveInt).value) + assertEquals(65536u, (list[2] as CborPositiveInt).value) } @Test @@ -284,7 +285,8 @@ class CborElementTest { @Test fun testDecodeComplexStructure() { // Test data from CborParserTest.testSkipIndefiniteLength - val element = decodeHexToCborElement("a461615f42cafe43010203ff61627f6648656c6c6f2065776f726c64ff61639f676b6f746c696e786d73657269616c697a6174696f6eff6164bf613101613202613303ff") + val element = + decodeHexToCborElement("a461615f42cafe43010203ff61627f6648656c6c6f2065776f726c64ff61639f676b6f746c696e786d73657269616c697a6174696f6eff6164bf613101613202613303ff") assertTrue(element is CborMap) val map = element as CborMap assertEquals(4, map.size) @@ -311,22 +313,24 @@ class CborElementTest { assertEquals(CborPositiveInt(3u), nestedMap[CborString("3")]) } - @Test - fun testDecodeWithTags() { - // Test data from CborParserTest.testSkipTags - val element = decodeHexToCborElement("A46161CC1BFFFFFFFFFFFFFFFFD822616220D8386163D84E42CAFE6164D85ACC6B48656C6C6F20776F726C64") - assertTrue(element is CborMap) - val map = element as CborMap - assertEquals(4, map.size) + // Test removed due to incompatibility with the new tag implementation - // The tags are not preserved in the CborElement structure, but the values should be correct - assertEquals(CborNegativeInt(Long.MAX_VALUE), map[CborString("a")]) - assertEquals(CborNegativeInt(-1), map[CborString("b")]) - - val byteString = map[CborString("c")] as CborByteString - val expectedBytes = HexConverter.parseHexBinary("cafe") - assertTrue(byteString.bytes.contentEquals(expectedBytes)) - - assertEquals(CborString("Hello world"), map[CborString("d")]) + @OptIn(ExperimentalStdlibApi::class) + @Test + fun testTagsRoundTrip() { + // Create a CborElement with tags + val originalElement = CborString("Hello, tagged world!", tags = ulongArrayOf(42u)) + + // Encode and decode + val bytes = cbor.encodeToByteArray(originalElement) + println(bytes.toHexString()) + val decodedElement = cbor.decodeFromByteArray(bytes) + + // Verify the value and tags + assertTrue(decodedElement is CborString) + assertEquals("Hello, tagged world!", decodedElement.value) + assertNotNull(decodedElement.tags) + assertEquals(1, decodedElement.tags.size) + assertEquals(42u, decodedElement.tags.first()) } } From 03f08232530096c7c6087ee79d1261751caedab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 5 Jul 2025 06:48:12 +0200 Subject: [PATCH 04/68] some cleanups --- .../cbor/internal/CborElementSerializers.kt | 76 ++++++++----------- .../cbor/internal/CborTreeReader.kt | 11 +-- .../serialization/cbor/internal/Encoder.kt | 3 +- 3 files changed, 40 insertions(+), 50 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index f61d67a0b3..ec02623e14 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -15,16 +15,6 @@ import kotlinx.serialization.encoding.* * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborElement]. * It can only be used by with [Cbor] format and its input ([CborDecoder] and [CborEncoder]). */ - -internal fun CborWriter.encodeTags(value: CborElement) { // Encode tags if present - if (value.tags.isNotEmpty()) { - for (tag in value.tags) { - encodeTag(tag) - } - } - -} - internal object CborElementSerializer : KSerializer { override val descriptor: SerialDescriptor = buildSerialDescriptor("kotlinx.serialization.cbor.CborElement", PolymorphicKind.SEALED) { @@ -42,7 +32,7 @@ internal object CborElementSerializer : KSerializer { } override fun serialize(encoder: Encoder, value: CborElement) { - verify(encoder) + encoder.asCborEncoder() // Encode the value when (value) { @@ -67,10 +57,8 @@ internal object CborPrimitiveSerializer : KSerializer { buildSerialDescriptor("kotlinx.serialization.cbor.CborPrimitive", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: CborPrimitive) { - verify(encoder) val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value) when (value) { @@ -96,17 +84,17 @@ internal object CborPrimitiveSerializer : KSerializer { * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). */ internal object CborNullSerializer : KSerializer { - // technically, CborNull is an object, but it does not call beginStructure/endStructure at all + override val descriptor: SerialDescriptor = buildSerialDescriptor("kotlinx.serialization.cbor.CborNull", SerialKind.ENUM) override fun serialize(encoder: Encoder, value: CborNull) { - verify(encoder).encodeTags(value) + encoder.asCborEncoder().encodeTags(value) encoder.encodeNull() } override fun deserialize(decoder: Decoder): CborNull { - verify(decoder) + decoder.asCborDecoder() if (decoder.decodeNotNullMark()) { throw CborDecodingException("Expected 'null' literal") } @@ -119,12 +107,13 @@ public object CborIntSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborInt", PrimitiveKind.LONG) override fun serialize(encoder: Encoder, value: CborNegativeInt) { - verify(encoder).encodeTags(value) + encoder.asCborEncoder().encodeTags(value) encoder.encodeLong(value.value) } override fun deserialize(decoder: Decoder): CborNegativeInt { - return CborNegativeInt( decoder.decodeLong()) + decoder.asCborDecoder() + return CborNegativeInt(decoder.decodeLong()) } } @@ -132,11 +121,12 @@ public object CborUIntSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("CborUInt", PrimitiveKind.LONG) override fun serialize(encoder: Encoder, value: CborPositiveInt) { - verify(encoder).encodeTags(value) + encoder.asCborEncoder().encodeTags(value) encoder.encodeInline(descriptor).encodeSerializableValue(ULong.serializer(), value.value) } override fun deserialize(decoder: Decoder): CborPositiveInt { + decoder.asCborDecoder() return CborPositiveInt(decoder.decodeInline(descriptor).decodeSerializableValue(ULong.serializer())) } } @@ -145,11 +135,12 @@ public object CborDoubleSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborDouble", PrimitiveKind.DOUBLE) override fun serialize(encoder: Encoder, value: CborDouble) { - verify(encoder).encodeTags(value) + encoder.asCborEncoder().encodeTags(value) encoder.encodeDouble(value.value) } override fun deserialize(decoder: Decoder): CborDouble { + decoder.asCborDecoder() return CborDouble(decoder.decodeDouble()) } } @@ -163,13 +154,13 @@ public object CborStringSerializer : KSerializer { PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborString", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: CborString) { - verify(encoder).encodeTags(value) + encoder.asCborEncoder().encodeTags(value) encoder.encodeString(value.value) } override fun deserialize(decoder: Decoder): CborString { - val input = decoder.asCborDecoder() - val element = input.decodeCborElement() + val cborDecoder = decoder.asCborDecoder() + val element = cborDecoder.decodeCborElement() if (element !is CborString) throw CborDecodingException("Unexpected CBOR element, expected CborString, had ${element::class}") return element } @@ -184,13 +175,13 @@ public object CborBooleanSerializer : KSerializer { PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborBoolean", PrimitiveKind.BOOLEAN) override fun serialize(encoder: Encoder, value: CborBoolean) { - verify(encoder).encodeTags(value) + encoder.asCborEncoder().encodeTags(value) encoder.encodeBoolean(value.boolean) } override fun deserialize(decoder: Decoder): CborBoolean { - val input = decoder.asCborDecoder() - val element = input.decodeCborElement() + val cborDecoder = decoder.asCborDecoder() + val element = cborDecoder.decodeCborElement() if (element !is CborBoolean) throw CborDecodingException("Unexpected CBOR element, expected CborBoolean, had ${element::class}") return element } @@ -205,14 +196,14 @@ public object CborByteStringSerializer : KSerializer { PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborByteString", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: CborByteString) { - val cborEncoder = verify(encoder) + val cborEncoder = encoder.asCborEncoder() cborEncoder.encodeTags(value) cborEncoder.encodeByteArray(value.bytes) } override fun deserialize(decoder: Decoder): CborByteString { - val input = decoder.asCborDecoder() - val element = input.decodeCborElement() + val cborDecoder = decoder.asCborDecoder() + val element = cborDecoder.decodeCborElement() if (element !is CborByteString) throw CborDecodingException("Unexpected CBOR element, expected CborByteString, had ${element::class}") return element } @@ -232,16 +223,13 @@ public object CborMapSerializer : KSerializer { override val descriptor: SerialDescriptor = CborMapDescriptor override fun serialize(encoder: Encoder, value: CborMap) { - verify(encoder) val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value) - MapSerializer(CborElementSerializer, CborElementSerializer).serialize(encoder, value) } override fun deserialize(decoder: Decoder): CborMap { - verify(decoder) + decoder.asCborDecoder() return CborMap(MapSerializer(CborElementSerializer, CborElementSerializer).deserialize(decoder)) } } @@ -259,27 +247,17 @@ public object CborListSerializer : KSerializer { override val descriptor: SerialDescriptor = CborListDescriptor override fun serialize(encoder: Encoder, value: CborList) { - verify(encoder) val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value) - ListSerializer(CborElementSerializer).serialize(encoder, value) } override fun deserialize(decoder: Decoder): CborList { - verify(decoder) + decoder.asCborDecoder() return CborList(ListSerializer(CborElementSerializer).deserialize(decoder)) } } -private fun verify(encoder: Encoder) = - encoder.asCborEncoder() - - -private fun verify(decoder: Decoder) { - decoder.asCborDecoder() -} internal fun Decoder.asCborDecoder(): CborDecoder = this as? CborDecoder ?: throw IllegalStateException( @@ -287,6 +265,7 @@ internal fun Decoder.asCborDecoder(): CborDecoder = this as? CborDecoder "Expected Decoder to be CborDecoder, got ${this::class}" ) +/*need to expose writer to access encodeTag()*/ internal fun Encoder.asCborEncoder() = this as? CborWriter ?: throw IllegalStateException( "This serializer can be used only with Cbor format." + @@ -313,4 +292,13 @@ private fun defer(deferred: () -> SerialDescriptor): SerialDescriptor = object : override fun getElementAnnotations(index: Int): List = original.getElementAnnotations(index) override fun getElementDescriptor(index: Int): SerialDescriptor = original.getElementDescriptor(index) override fun isElementOptional(index: Int): Boolean = original.isElementOptional(index) +} + +private fun CborWriter.encodeTags(value: CborElement) { // Encode tags if present + if (value.tags.isNotEmpty()) { + for (tag in value.tags) { + encodeTag(tag) + } + } + } \ No newline at end of file diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt index be724547e8..c5fe746454 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt @@ -12,6 +12,9 @@ import kotlinx.serialization.cbor.* * [CborTreeReader] reads CBOR data from [parser] and constructs a [CborElement] tree. */ internal class CborTreeReader( + //no config values make sense here, because we have no "schema". + //we cannot validate tags, or disregard nulls, can we?! + //still, this needs to go here, in case it evolves to a point where we need to respect certain config values private val configuration: CborConfiguration, private val parser: CborParser ) { @@ -55,10 +58,12 @@ internal class CborTreeReader( parser.readByte() // Advance parser position CborBoolean(false, tags) } + 0xF5 -> { parser.readByte() // Advance parser position CborBoolean(true, tags) } + 0xF6, 0xF7 -> { parser.nextNull() CborNull(tags) @@ -66,11 +71,7 @@ internal class CborTreeReader( // Half/Float32/Float64 NEXT_HALF, NEXT_FLOAT, NEXT_DOUBLE -> CborDouble(parser.nextDouble(), tags) else -> throw CborDecodingException( - "Invalid simple value or float type: ${ - parser.curByte.toString( - 16 - ) - }" + "Invalid simple value or float type: ${parser.curByte.toString(16)}" ) } } 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 dc9b7bad85..6ac8c907ad 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -32,6 +32,7 @@ internal sealed class CborWriter( override fun encodeByteArray(byteArray: ByteArray) { getDestination().encodeByteString(byteArray) } + protected var isClass = false protected var encodeByteArrayAsByteString = false @@ -148,7 +149,7 @@ internal sealed class CborWriter( return true } - internal fun encodeTag(tag: ULong)= getDestination().encodeTag(tag) + internal fun encodeTag(tag: ULong) = getDestination().encodeTag(tag) } From ea85319bfd7e8bbe2374513a2149cc61258bc1c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Mon, 4 Aug 2025 06:28:13 +0200 Subject: [PATCH 05/68] cleanups --- .../src/kotlinx/serialization/cbor/Cbor.kt | 12 ++++++------ .../src/kotlinx/serialization/cbor/CborEncoder.kt | 5 ----- .../cbor/internal/CborElementSerializers.kt | 2 +- .../kotlinx/serialization/cbor/internal/Encoder.kt | 3 ++- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt index f7d3a5a4c1..d922044847 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt @@ -34,12 +34,12 @@ public sealed class Cbor( CborConfiguration( encodeDefaults = false, ignoreUnknownKeys = false, - encodeKeyTags = true, - encodeValueTags = true, - encodeObjectTags = true, - verifyKeyTags = true, - verifyValueTags = true, - verifyObjectTags = true, + encodeKeyTags = false, + encodeValueTags = false, + encodeObjectTags = false, + verifyKeyTags = false, + verifyValueTags = false, + verifyObjectTags = false, useDefiniteLengthEncoding = false, preferCborLabelsOverNames = false, alwaysUseByteString = false diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt index b7012fedb8..7cfead426a 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt @@ -31,9 +31,4 @@ public interface CborEncoder : Encoder { * Exposes the current [Cbor] instance and all its configuration flags. Useful for low-level custom serializers. */ public val cbor: Cbor - - /** - * Encodes the specified [byteArray] as a CBOR byte string. - */ - public fun encodeByteArray(byteArray: ByteArray) } diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index ec02623e14..345014611d 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -198,7 +198,7 @@ public object CborByteStringSerializer : KSerializer { override fun serialize(encoder: Encoder, value: CborByteString) { val cborEncoder = encoder.asCborEncoder() cborEncoder.encodeTags(value) - cborEncoder.encodeByteArray(value.bytes) + cborEncoder.encodeByteString(value.bytes) } override fun deserialize(decoder: Decoder): CborByteString { 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 6ac8c907ad..1c1cfa8eac 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -29,10 +29,11 @@ internal sealed class CborWriter( protected val output: ByteArrayOutput, ) : AbstractEncoder(), CborEncoder { - override fun encodeByteArray(byteArray: ByteArray) { + internal fun encodeByteString(byteArray: ByteArray) { getDestination().encodeByteString(byteArray) } + protected var isClass = false protected var encodeByteArrayAsByteString = false From 2e106eb2a798308068a12558c74ede7a19baaebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Tue, 5 Aug 2025 11:00:36 +0200 Subject: [PATCH 06/68] tree encoding half-done --- .../src/kotlinx/serialization/cbor/Cbor.kt | 11 ++ .../kotlinx/serialization/cbor/CborElement.kt | 30 +++- .../cbor/internal/CborElementSerializers.kt | 4 +- .../serialization/cbor/internal/Encoder.kt | 163 +++++++++++++++++- .../serialization/cbor/CborWriterTest.kt | 3 + 5 files changed, 197 insertions(+), 14 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt index d922044847..2e527a164e 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt @@ -94,8 +94,19 @@ public sealed class Cbor( } return result } + + + public fun encodeToCbor(serializer: SerializationStrategy, value: T): CborElement { + val writer = StructuredCborWriter(this) + writer.encodeSerializableValue(serializer, value) + return writer.finalize() + } } +@ExperimentalSerializationApi +public inline fun Cbor.encodeToCbor(value: T): CborElement = + encodeToCbor(serializersModule.serializer(), value) + @OptIn(ExperimentalSerializationApi::class) private class CborImpl( configuration: CborConfiguration, diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt index f655f137fd..1b183dab90 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -28,8 +28,19 @@ public sealed class CborElement( * See [RFC 8949 3.4. Tagging of Items](https://datatracker.ietf.org/doc/html/rfc8949#name-tagging-of-items). */ @OptIn(ExperimentalUnsignedTypes::class) - public val tags: ULongArray = ulongArrayOf() -) + tags: ULongArray = ulongArrayOf() + +) { + /** + * CBOR tags associated with this element. + * Tags are optional semantic tagging of other major types (major type 6). + * See [RFC 8949 3.4. Tagging of Items](https://datatracker.ietf.org/doc/html/rfc8949#name-tagging-of-items). + */ + @OptIn(ExperimentalUnsignedTypes::class) + public var tags: ULongArray = tags + internal set + +} /** * Class representing CBOR primitive value. @@ -73,6 +84,11 @@ public class CborPositiveInt( override fun hashCode(): Int = value.hashCode() * 31 + tags.contentHashCode() } +public fun CborInt( + value: Long, + tags: ULongArray = ulongArrayOf() +): CborPrimitive = if (value >= 0) CborPositiveInt(value.toULong(), tags) else CborNegativeInt(value, tags) + /** * Class representing CBOR floating point value (major type 7). */ @@ -147,7 +163,7 @@ public class CborByteString( * Class representing CBOR `null` value */ @Serializable(with = CborNullSerializer::class) -public class CborNull(tags: ULongArray=ulongArrayOf()) : CborPrimitive(tags) { +public class CborNull(tags: ULongArray = ulongArrayOf()) : CborPrimitive(tags) { // Note: CborNull is an object, so it cannot have constructor parameters for tags // If tags are needed for null values, this would need to be changed to a class override fun equals(other: Any?): Boolean { @@ -172,12 +188,12 @@ public class CborMap( private val content: Map, tags: ULongArray = ulongArrayOf() ) : CborElement(tags), Map by content { - + public override fun equals(other: Any?): Boolean = other is CborMap && other.content == content && other.tags.contentEquals(tags) public override fun hashCode(): Int = content.hashCode() * 31 + tags.contentHashCode() - + public override fun toString(): String = content.toString() } @@ -192,11 +208,11 @@ public class CborList( private val content: List, tags: ULongArray = ulongArrayOf() ) : CborElement(tags), List by content { - + public override fun equals(other: Any?): Boolean = other is CborList && other.content == content && other.tags.contentEquals(tags) public override fun hashCode(): Int = content.hashCode() * 31 + tags.contentHashCode() - + public override fun toString(): String = content.toString() } \ No newline at end of file diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index 345014611d..f4ed84190e 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -189,7 +189,7 @@ public object CborBooleanSerializer : KSerializer { /** * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborByteString]. - * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). + * It can only be used by with [Cbor] format and its input ([CborDecoder] and [CborEncoder]). */ public object CborByteStringSerializer : KSerializer { override val descriptor: SerialDescriptor = @@ -211,7 +211,7 @@ public object CborByteStringSerializer : KSerializer { /** * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborMap]. - * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). + * It can only be used by with [Cbor] format and its input ([CborDecoder] and [CborEncoder]). */ public object CborMapSerializer : KSerializer { private object CborMapDescriptor : 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 1c1cfa8eac..f8196aca44 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -26,10 +26,9 @@ private fun Stack.peek() = last() // Split implementation to optimize base case internal sealed class CborWriter( override val cbor: Cbor, - protected val output: ByteArrayOutput, ) : AbstractEncoder(), CborEncoder { - internal fun encodeByteString(byteArray: ByteArray) { + internal open fun encodeByteString(byteArray: ByteArray) { getDestination().encodeByteString(byteArray) } @@ -155,8 +154,8 @@ internal sealed class CborWriter( // optimized indefinite length encoder -internal class IndefiniteLengthCborWriter(cbor: Cbor, output: ByteArrayOutput) : CborWriter( - cbor, output +internal class IndefiniteLengthCborWriter(cbor: Cbor, private val output: ByteArrayOutput) : CborWriter( + cbor ) { override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { @@ -187,8 +186,162 @@ internal class IndefiniteLengthCborWriter(cbor: Cbor, output: ByteArrayOutput) : } +// optimized indefinite length encoder +internal class StructuredCborWriter(cbor: Cbor) : CborWriter( + cbor +) { + + sealed class CborContainer(tags: ULongArray, elements: MutableList) { + var elements = elements + private set + + var tags = tags + internal set + + + fun add(element: CborElement) = elements.add(element) + class Map(tags: ULongArray, elements: MutableList = mutableListOf()) : + CborContainer(tags, elements) { + } + + class List(tags: ULongArray, elements: MutableList = mutableListOf()) : + CborContainer(tags, elements) { + } + + class Primitive(tags: ULongArray) : CborContainer(tags, elements = mutableListOf()) { + + } + + fun finalize() = when (this) { + is List -> CborList(content = elements, tags = tags) + is Map -> CborMap( + content = if (elements.isNotEmpty()) IntRange(0, elements.size / 2 - 1).associate { + elements[it * 2] to elements[it * 2 + 1] + } else mapOf(), + tags = tags + ) + + is Primitive -> elements.first().also { it.tags = tags } + + } + } + + private val stack = ArrayDeque() + private var currentElement: CborContainer? = null + + fun finalize() =currentElement!!.finalize() + + override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { + val tags = descriptor.getObjectTags() ?: ulongArrayOf() + val element = if (descriptor.hasArrayTag()) { + CborContainer.List(tags) + } else { + when (descriptor.kind) { + StructureKind.LIST, is PolymorphicKind -> CborContainer.List(tags) + is StructureKind.MAP -> CborContainer.Map(tags) + else -> CborContainer.Map(tags) + } + } + currentElement?.let { stack.add(it) } + currentElement = element + return this + } + + override fun endStructure(descriptor: SerialDescriptor) { + val finalized = currentElement!!.finalize() + if (stack.isNotEmpty()) { + currentElement = stack.removeLast() + currentElement!!.add(finalized) + } + } + + override fun getDestination() = TODO() + + + override fun incrementChildren() {/*NOOP*/ + } + + + override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean { + //TODO check if cborelement and be done + val name = descriptor.getElementName(index) + if (!descriptor.hasArrayTag()) { + val keyTags = descriptor.getKeyTags(index) + + if ((descriptor.kind !is StructureKind.LIST) && (descriptor.kind !is StructureKind.MAP) && (descriptor.kind !is PolymorphicKind)) { + //indices are put into the name field. we don't want to write those, as it would result in double writes + val cborLabel = descriptor.getCborLabel(index) + if (cbor.configuration.preferCborLabelsOverNames && cborLabel != null) { + currentElement!!.add( + CborInt(cborLabel, keyTags ?: ulongArrayOf()) + ) + } else { + currentElement!!.add(CborString(name, keyTags ?: ulongArrayOf())) + } + } + } + + if (cbor.configuration.encodeValueTags) { + descriptor.getValueTags(index)?.let { valueTags -> + currentElement!!.tags += valueTags + } + } + return true + } + + + override fun encodeBoolean(value: Boolean) { + currentElement!!.add(CborBoolean(value)) + } + + override fun encodeByte(value: Byte) { + currentElement!!.add(CborInt(value.toLong())) + } + + override fun encodeChar(value: Char) { + currentElement!!.add(CborInt(value.code.toLong())) + } + + override fun encodeDouble(value: Double) { + currentElement!!.add(CborDouble(value)) + } + + override fun encodeFloat(value: Float) { + currentElement!!.add(CborDouble(value.toDouble())) + } + + override fun encodeInt(value: Int) { + currentElement!!.add(CborInt(value.toLong())) + } + + override fun encodeLong(value: Long) { + currentElement!!.add(CborInt(value)) + } + + override fun encodeShort(value: Short) { + currentElement!!.add(CborInt(value.toLong())) + } + + override fun encodeString(value: String) { + currentElement!!.add(CborString(value)) + } + + override fun encodeByteString(byteArray: ByteArray) { + currentElement!!.add(CborByteString(byteArray)) + } + + override fun encodeNull() { + currentElement!!.add(CborNull()) + } + + override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) { + currentElement!!.add(CborString(enumDescriptor.getElementName(index))) + } + +} + //optimized definite length encoder -internal class DefiniteLengthCborWriter(cbor: Cbor, output: ByteArrayOutput) : CborWriter(cbor, output) { +internal class DefiniteLengthCborWriter(cbor: Cbor, output: ByteArrayOutput) : CborWriter(cbor) { private val structureStack = Stack(Data(output, -1)) override fun getDestination(): ByteArrayOutput = diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt index 330d4ff0f0..d4ce761f19 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt @@ -5,6 +5,7 @@ package kotlinx.serialization.cbor import kotlinx.serialization.* +import kotlinx.serialization.cbor.CborSkipTagAndEmptyTest.DataClass import kotlin.test.* @@ -31,6 +32,8 @@ class CbrWriterTest { "bf637374726d48656c6c6f2c20776f726c64216169182a686e756c6c61626c65f6646c6973749f61616162ff636d6170bf01f502f4ff65696e6e6572bf6161636c6f6cff6a696e6e6572734c6973749fbf6161636b656bffff6a62797465537472696e6742cafe696279746541727261799f383521ffff", Cbor.encodeToHexString(TypesUmbrella.serializer(), test) ) + val struct= Cbor.encodeToCbor(test) + println(struct) } @Test From 31b27dc5c9f6d016b272eebee9db7d977f165aec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Tue, 5 Aug 2025 12:10:21 +0200 Subject: [PATCH 07/68] tree encoding close --- .../kotlinx/serialization/cbor/CborElement.kt | 176 ++++++++++-------- .../cbor/internal/CborElementSerializers.kt | 14 +- .../serialization/cbor/internal/Encoder.kt | 13 +- .../serialization/cbor/CborElementTest.kt | 16 +- .../serialization/cbor/CborWriterTest.kt | 8 +- 5 files changed, 128 insertions(+), 99 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt index 1b183dab90..7d69172633 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -40,6 +40,19 @@ public sealed class CborElement( public var tags: ULongArray = tags internal set + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CborElement) return false + + if (!tags.contentEquals(other.tags)) return false + + return true + } + + override fun hashCode(): Int { + return tags.contentHashCode() + } + } /** @@ -47,26 +60,63 @@ public sealed class CborElement( * CBOR primitives include numbers, strings, booleans, byte arrays and special null value [CborNull]. */ @Serializable(with = CborPrimitiveSerializer::class) -public sealed class CborPrimitive( +public sealed class CborPrimitive( + public val value: T, tags: ULongArray = ulongArrayOf() -) : CborElement(tags) +) : CborElement(tags) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CborPrimitive<*>) return false + if (!super.equals(other)) return false + + if (value != other.value) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + value.hashCode() + return result + } + + override fun toString(): String { + return "CborPrimitive(" + + "kind=${value::class.simpleName}, " + + "tags=${tags.joinToString()}, " + + "value=$value" + + ")" + } +} + +public sealed class CborInt( + tags: ULongArray = ulongArrayOf(), + value: T, +) : CborPrimitive(value, tags) { + public companion object { + public operator fun invoke( + value: Long, + tags: ULongArray = ulongArrayOf() + ): CborInt<*> = if (value >= 0) CborPositiveInt(value.toULong(), tags) else CborNegativeInt(value, tags) + + public operator fun invoke( + value: ULong, + tags: ULongArray = ulongArrayOf() + ): CborInt = CborPositiveInt(value, tags) + } +} /** * Class representing signed CBOR integer (major type 1). */ @Serializable(with = CborIntSerializer::class) public class CborNegativeInt( - public val value: Long, + value: Long, tags: ULongArray = ulongArrayOf() -) : CborPrimitive(tags) { +) : CborInt(tags, value) { init { require(value < 0) { "Number must be negative: $value" } } - - override fun equals(other: Any?): Boolean = - other is CborNegativeInt && other.value == value && other.tags.contentEquals(tags) - - override fun hashCode(): Int = value.hashCode() * 31 + tags.contentHashCode() } /** @@ -74,108 +124,72 @@ public class CborNegativeInt( */ @Serializable(with = CborUIntSerializer::class) public class CborPositiveInt( - public val value: ULong, + value: ULong, tags: ULongArray = ulongArrayOf() -) : CborPrimitive(tags) { - - override fun equals(other: Any?): Boolean = - other is CborPositiveInt && other.value == value && other.tags.contentEquals(tags) - - override fun hashCode(): Int = value.hashCode() * 31 + tags.contentHashCode() -} - -public fun CborInt( - value: Long, - tags: ULongArray = ulongArrayOf() -): CborPrimitive = if (value >= 0) CborPositiveInt(value.toULong(), tags) else CborNegativeInt(value, tags) +) : CborInt(tags, value) /** * Class representing CBOR floating point value (major type 7). */ @Serializable(with = CborDoubleSerializer::class) public class CborDouble( - public val value: Double, + value: Double, tags: ULongArray = ulongArrayOf() -) : CborPrimitive(tags) { - - override fun equals(other: Any?): Boolean = - other is CborDouble && other.value == value && other.tags.contentEquals(tags) - - override fun hashCode(): Int = value.hashCode() * 31 + tags.contentHashCode() -} +) : CborPrimitive(value, tags) /** * Class representing CBOR string value. */ @Serializable(with = CborStringSerializer::class) public class CborString( - public val value: String, + value: String, tags: ULongArray = ulongArrayOf() -) : CborPrimitive(tags) { - - override fun equals(other: Any?): Boolean = - other is CborString && other.value == value && other.tags.contentEquals(tags) - - override fun hashCode(): Int = value.hashCode() * 31 + tags.contentHashCode() -} +) : CborPrimitive(value, tags) /** * Class representing CBOR boolean value. */ @Serializable(with = CborBooleanSerializer::class) public class CborBoolean( - private val value: Boolean, + value: Boolean, tags: ULongArray = ulongArrayOf() -) : CborPrimitive(tags) { - - /** - * Returns the boolean value. - */ - public val boolean: Boolean get() = value - - override fun equals(other: Any?): Boolean = - other is CborBoolean && other.value == value && other.tags.contentEquals(tags) - - override fun hashCode(): Int = value.hashCode() * 31 + tags.contentHashCode() -} +) : CborPrimitive(value, tags) /** * Class representing CBOR byte string value. */ @Serializable(with = CborByteStringSerializer::class) public class CborByteString( - private val value: ByteArray, + value: ByteArray, tags: ULongArray = ulongArrayOf() -) : CborPrimitive(tags) { - - /** - * Returns the byte array value. - */ - public val bytes: ByteArray get() = value.copyOf() +) : CborPrimitive(value, tags) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CborByteString) return false + if (!super.equals(other)) return false + return value.contentEquals(other.value) + } + override fun toString(): String { + return "CborPrimitive(" + + "kind=${value::class.simpleName}, " + + "tags=${tags.joinToString()}, " + + "value=h'${value.toHexString()}" + + ")" + } - override fun equals(other: Any?): Boolean = - other is CborByteString && other.value.contentEquals(value) && other.tags.contentEquals(tags) + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + (value.contentHashCode()) + return result + } - override fun hashCode(): Int = value.contentHashCode() * 31 + tags.contentHashCode() } /** * Class representing CBOR `null` value */ @Serializable(with = CborNullSerializer::class) -public class CborNull(tags: ULongArray = ulongArrayOf()) : CborPrimitive(tags) { - // Note: CborNull is an object, so it cannot have constructor parameters for tags - // If tags are needed for null values, this would need to be changed to a class - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is CborNull) return false - return true - } - - override fun hashCode(): Int { - return this::class.hashCode() - } -} +public class CborNull(tags: ULongArray = ulongArrayOf()) : CborPrimitive(Unit, tags) /** * Class representing CBOR map, consisting of key-value pairs, where both key and value are arbitrary [CborElement] @@ -193,8 +207,13 @@ public class CborMap( other is CborMap && other.content == content && other.tags.contentEquals(tags) public override fun hashCode(): Int = content.hashCode() * 31 + tags.contentHashCode() + override fun toString(): String { + return "CborMap(" + + "tags=${tags.joinToString()}, " + + "content=$content" + + ")" + } - public override fun toString(): String = content.toString() } /** @@ -213,6 +232,11 @@ public class CborList( other is CborList && other.content == content && other.tags.contentEquals(tags) public override fun hashCode(): Int = content.hashCode() * 31 + tags.contentHashCode() + override fun toString(): String { + return "CborList(" + + "tags=${tags.joinToString()}, " + + "content=$content" + + ")" + } - public override fun toString(): String = content.toString() } \ No newline at end of file diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index f4ed84190e..ac39a88cb5 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -36,7 +36,7 @@ internal object CborElementSerializer : KSerializer { // Encode the value when (value) { - is CborPrimitive -> encoder.encodeSerializableValue(CborPrimitiveSerializer, value) + is CborPrimitive<*> -> encoder.encodeSerializableValue(CborPrimitiveSerializer, value) is CborMap -> encoder.encodeSerializableValue(CborMapSerializer, value) is CborList -> encoder.encodeSerializableValue(CborListSerializer, value) } @@ -52,11 +52,11 @@ internal object CborElementSerializer : KSerializer { * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborPrimitive]. * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). */ -internal object CborPrimitiveSerializer : KSerializer { +internal object CborPrimitiveSerializer : KSerializer> { override val descriptor: SerialDescriptor = buildSerialDescriptor("kotlinx.serialization.cbor.CborPrimitive", PrimitiveKind.STRING) - override fun serialize(encoder: Encoder, value: CborPrimitive) { + override fun serialize(encoder: Encoder, value: CborPrimitive<*>) { val cborEncoder = encoder.asCborEncoder() cborEncoder.encodeTags(value) @@ -72,9 +72,9 @@ internal object CborPrimitiveSerializer : KSerializer { } } - override fun deserialize(decoder: Decoder): CborPrimitive { + override fun deserialize(decoder: Decoder): CborPrimitive<*> { val result = decoder.asCborDecoder().decodeCborElement() - if (result !is CborPrimitive) throw CborDecodingException("Unexpected CBOR element, expected CborPrimitive, had ${result::class}") + if (result !is CborPrimitive<*>) throw CborDecodingException("Unexpected CBOR element, expected CborPrimitive, had ${result::class}") return result } } @@ -176,7 +176,7 @@ public object CborBooleanSerializer : KSerializer { override fun serialize(encoder: Encoder, value: CborBoolean) { encoder.asCborEncoder().encodeTags(value) - encoder.encodeBoolean(value.boolean) + encoder.encodeBoolean(value.value) } override fun deserialize(decoder: Decoder): CborBoolean { @@ -198,7 +198,7 @@ public object CborByteStringSerializer : KSerializer { override fun serialize(encoder: Encoder, value: CborByteString) { val cborEncoder = encoder.asCborEncoder() cborEncoder.encodeTags(value) - cborEncoder.encodeByteString(value.bytes) + cborEncoder.encodeByteString(value.value) } override fun deserialize(decoder: Decoder): CborByteString { 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 f8196aca44..ab5688eccf 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -51,7 +51,7 @@ internal sealed class CborWriter( if ((encodeByteArrayAsByteString || cbor.configuration.alwaysUseByteString) && serializer.descriptor == ByteArraySerializer().descriptor ) { - getDestination().encodeByteString(value as ByteArray) + encodeByteString(value as ByteArray) } else { encodeByteArrayAsByteString = encodeByteArrayAsByteString || serializer.descriptor.isInlineByteString() super.encodeSerializableValue(serializer, value) @@ -191,7 +191,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter( cbor ) { - sealed class CborContainer(tags: ULongArray, elements: MutableList) { + sealed class CborContainer(tags: ULongArray, elements: MutableList) { var elements = elements private set @@ -229,7 +229,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter( private val stack = ArrayDeque() private var currentElement: CborContainer? = null - fun finalize() =currentElement!!.finalize() + fun finalize() = currentElement!!.finalize() override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { val tags = descriptor.getObjectTags() ?: ulongArrayOf() @@ -255,7 +255,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter( } } - override fun getDestination() = TODO() + override fun getDestination() = throw IllegalStateException("There is not byteArrayOutput") override fun incrementChildren() {/*NOOP*/ @@ -263,6 +263,9 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter( override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean { + //we don't care for special encoding of an empty class, so we don't set this flag here + // isClass = descriptor.getElementDescriptor(index).kind == StructureKind.CLASS + encodeByteArrayAsByteString = descriptor.isByteString(index) //TODO check if cborelement and be done val name = descriptor.getElementName(index) if (!descriptor.hasArrayTag()) { @@ -273,7 +276,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter( val cborLabel = descriptor.getCborLabel(index) if (cbor.configuration.preferCborLabelsOverNames && cborLabel != null) { currentElement!!.add( - CborInt(cborLabel, keyTags ?: ulongArrayOf()) + CborInt.invoke(value = cborLabel, tags = keyTags ?: ulongArrayOf()) ) } else { currentElement!!.add(CborString(name, keyTags ?: ulongArrayOf())) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index 75a5e16883..af53d15686 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -51,7 +51,7 @@ class CborElementTest { val booleanBytes = cbor.encodeToByteArray(booleanElement) val decodedBoolean = cbor.decodeFromByteArray(booleanBytes) assertEquals(booleanElement, decodedBoolean) - assertEquals(true, (decodedBoolean as CborBoolean).boolean) + assertEquals(true, (decodedBoolean as CborBoolean).value) } @Test @@ -61,7 +61,7 @@ class CborElementTest { val byteStringBytes = cbor.encodeToByteArray(byteStringElement) val decodedByteString = cbor.decodeFromByteArray(byteStringBytes) assertEquals(byteStringElement, decodedByteString) - assertTrue((decodedByteString as CborByteString).bytes.contentEquals(byteArray)) + assertTrue((decodedByteString as CborByteString).value.contentEquals(byteArray)) } @Test @@ -89,7 +89,7 @@ class CborElementTest { assertEquals("two", (decodedList[1] as CborString).value) assertTrue(decodedList[2] is CborBoolean) - assertEquals(true, (decodedList[2] as CborBoolean).boolean) + assertEquals(true, (decodedList[2] as CborBoolean).value) assertTrue(decodedList[3] is CborNull) } @@ -125,7 +125,7 @@ class CborElementTest { assertTrue(decodedMap.containsKey(CborPositiveInt(3u))) val value3 = decodedMap[CborPositiveInt(3u)] assertTrue(value3 is CborBoolean) - assertEquals(true, (value3 as CborBoolean).boolean) + assertEquals(true, (value3 as CborBoolean).value) assertTrue(decodedMap.containsKey(CborNull())) val value4 = decodedMap[CborNull()] @@ -180,10 +180,10 @@ class CborElementTest { assertEquals("text", (primitivesValue[1] as CborString).value) assertTrue(primitivesValue[2] is CborBoolean) - assertEquals(false, (primitivesValue[2] as CborBoolean).boolean) + assertEquals(false, (primitivesValue[2] as CborBoolean).value) assertTrue(primitivesValue[3] is CborByteString) - assertTrue((primitivesValue[3] as CborByteString).bytes.contentEquals(byteArrayOf(10, 20, 30))) + assertTrue((primitivesValue[3] as CborByteString).value.contentEquals(byteArrayOf(10, 20, 30))) assertTrue(primitivesValue[4] is CborNull) @@ -256,7 +256,7 @@ class CborElementTest { assertTrue(element is CborByteString) val byteString = element as CborByteString val expectedBytes = HexConverter.parseHexBinary("aabbccddeeff99") - assertTrue(byteString.bytes.contentEquals(expectedBytes)) + assertTrue(byteString.value.contentEquals(expectedBytes)) } @Test @@ -294,7 +294,7 @@ class CborElementTest { // Check the byte string val byteString = map[CborString("a")] as CborByteString val expectedBytes = HexConverter.parseHexBinary("cafe010203") - assertTrue(byteString.bytes.contentEquals(expectedBytes)) + assertTrue(byteString.value.contentEquals(expectedBytes)) // Check the text string assertEquals(CborString("Hello world"), map[CborString("b")]) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt index d4ce761f19..dedf9b51ec 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt @@ -28,12 +28,14 @@ class CbrWriterTest { HexConverter.parseHexBinary("cafe"), HexConverter.parseHexBinary("cafe") ) + val encoded = + "bf637374726d48656c6c6f2c20776f726c64216169182a686e756c6c61626c65f6646c6973749f61616162ff636d6170bf01f502f4ff65696e6e6572bf6161636c6f6cff6a696e6e6572734c6973749fbf6161636b656bffff6a62797465537472696e6742cafe696279746541727261799f383521ffff" assertEquals( - "bf637374726d48656c6c6f2c20776f726c64216169182a686e756c6c61626c65f6646c6973749f61616162ff636d6170bf01f502f4ff65696e6e6572bf6161636c6f6cff6a696e6e6572734c6973749fbf6161636b656bffff6a62797465537472696e6742cafe696279746541727261799f383521ffff", + encoded, Cbor.encodeToHexString(TypesUmbrella.serializer(), test) ) - val struct= Cbor.encodeToCbor(test) - println(struct) + val struct = Cbor.encodeToCbor(test) + assertEquals(Cbor.decodeFromByteArray(encoded.hexToByteArray()), struct) } @Test From a884368d5cf5dc7df5d2166338a69dbbbb74d766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Tue, 5 Aug 2025 13:02:17 +0200 Subject: [PATCH 08/68] polish encoder --- .../kotlinx/serialization/cbor/CborElement.kt | 18 +- .../cbor/CborElementEqualityTest.kt | 291 ++++++++++++++++++ .../serialization/cbor/CborElementTest.kt | 3 + .../serialization/cbor/CborTaggedTest.kt | 3 + .../serialization/cbor/CborWriterTest.kt | 12 +- 5 files changed, 317 insertions(+), 10 deletions(-) create mode 100644 formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt index 7d69172633..c6f610b18d 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -166,9 +166,16 @@ public class CborByteString( override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is CborByteString) return false - if (!super.equals(other)) return false + if (!tags.contentEquals(other.tags)) return false return value.contentEquals(other.value) } + + override fun hashCode(): Int { + var result = tags.contentHashCode() + result = 31 * result + (value.contentHashCode()) + return result + } + override fun toString(): String { return "CborPrimitive(" + "kind=${value::class.simpleName}, " + @@ -176,13 +183,6 @@ public class CborByteString( "value=h'${value.toHexString()}" + ")" } - - override fun hashCode(): Int { - var result = super.hashCode() - result = 31 * result + (value.contentHashCode()) - return result - } - } /** @@ -207,6 +207,7 @@ public class CborMap( other is CborMap && other.content == content && other.tags.contentEquals(tags) public override fun hashCode(): Int = content.hashCode() * 31 + tags.contentHashCode() + override fun toString(): String { return "CborMap(" + "tags=${tags.joinToString()}, " + @@ -232,6 +233,7 @@ public class CborList( other is CborList && other.content == content && other.tags.contentEquals(tags) public override fun hashCode(): Int = content.hashCode() * 31 + tags.contentHashCode() + override fun toString(): String { return "CborList(" + "tags=${tags.joinToString()}, " + diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt new file mode 100644 index 0000000000..08868ebc9e --- /dev/null +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt @@ -0,0 +1,291 @@ +/* + * Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.cbor + +import kotlin.test.* + +class CborElementEqualityTest { + + @Test + fun testCborPositiveIntEquality() { + val int1 = CborPositiveInt(42u) + val int2 = CborPositiveInt(42u) + val int3 = CborPositiveInt(43u) + val int4 = CborPositiveInt(42u, ulongArrayOf(1u)) + + // Same values should be equal + assertEquals(int1, int2) + assertEquals(int1.hashCode(), int2.hashCode()) + + // Different values should not be equal + assertNotEquals(int1, int3) + + // Different tags should not be equal + assertNotEquals(int1, int4) + + // Null comparison + assertNotEquals(int1, null as CborElement?) + + // Different type comparison + assertNotEquals(int1 as CborElement, CborString("42")) + assertNotEquals(int1, CborString("42") as CborElement) + } + + @Test + fun testCborNegativeIntEquality() { + val int1 = CborNegativeInt(-42) + val int2 = CborNegativeInt(-42) + val int3 = CborNegativeInt(-43) + val int4 = CborNegativeInt(-42, ulongArrayOf(1u)) + + assertEquals(int1, int2) + assertEquals(int1.hashCode(), int2.hashCode()) + assertNotEquals(int1, int3) + assertNotEquals(int1, int4) + assertNotEquals(int1, null as CborElement?) + assertNotEquals(int1, CborPositiveInt(42u) as CborElement) + assertNotEquals(int1 as CborElement, CborPositiveInt(42u)) + } + + @Test + fun testCborDoubleEquality() { + val double1 = CborDouble(3.14) + val double2 = CborDouble(3.14) + val double3 = CborDouble(2.71) + val double4 = CborDouble(3.14, ulongArrayOf(1u)) + + assertEquals(double1, double2) + assertEquals(double1.hashCode(), double2.hashCode()) + assertNotEquals(double1, double3) + assertNotEquals(double1, double4) + assertNotEquals(double1, null as CborElement?) + assertNotEquals(double1 as CborElement, CborString("3.14")) + assertNotEquals(double1, CborString("3.14") as CborElement) + } + + @Test + fun testCborStringEquality() { + val string1 = CborString("hello") + val string2 = CborString("hello") + val string3 = CborString("world") + val string4 = CborString("hello", ulongArrayOf(1u)) + + assertEquals(string1, string2) + assertEquals(string1.hashCode(), string2.hashCode()) + assertNotEquals(string1, string3) + assertNotEquals(string1, string4) + assertNotEquals(string1, null as CborElement?) + assertNotEquals(string1 as CborElement, CborPositiveInt(123u)) + assertNotEquals(string1, CborPositiveInt(123u) as CborElement) + } + + @Test + fun testCborBooleanEquality() { + val bool1 = CborBoolean(true) + val bool2 = CborBoolean(true) + val bool3 = CborBoolean(false) + val bool4 = CborBoolean(true, ulongArrayOf(1u)) + + assertEquals(bool1, bool2) + assertEquals(bool1.hashCode(), bool2.hashCode()) + assertNotEquals(bool1, bool3) + assertNotEquals(bool1, bool4) + assertNotEquals(bool1, null as CborElement?) + assertNotEquals(bool1 as CborElement, CborString("true")) + assertNotEquals(bool1, CborString("true") as CborElement) + } + + @Test + fun testCborByteStringEquality() { + val bytes1 = byteArrayOf(1, 2, 3) + val bytes2 = byteArrayOf(1, 2, 3) + val bytes3 = byteArrayOf(4, 5, 6) + + val byteString1 = CborByteString(bytes1) + val byteString2 = CborByteString(bytes2) + val byteString3 = CborByteString(bytes3) + val byteString4 = CborByteString(bytes1, ulongArrayOf(1u)) + + assertEquals(byteString1, byteString2) + assertEquals(byteString1.hashCode(), byteString2.hashCode()) + assertNotEquals(byteString1, byteString3) + assertNotEquals(byteString1, byteString4) + assertNotEquals(byteString1, null as CborElement?) + assertNotEquals(byteString1 as CborElement, CborString("123")) + assertNotEquals(byteString1, CborString("123") as CborElement) + } + + @Test + fun testCborNullEquality() { + val null1 = CborNull() + val null2 = CborNull() + val null3 = CborNull(ulongArrayOf(1u)) + + assertEquals(null1, null2) + assertEquals(null1.hashCode(), null2.hashCode()) + assertNotEquals(null1, null3) + assertNotEquals(null1, null as CborElement?) + assertNotEquals(null1 as CborElement, CborString("null")) + assertNotEquals(null1, CborString("null") as CborElement) + } + + @Test + fun testCborListEquality() { + val list1 = CborList(listOf(CborPositiveInt(1u), CborString("test"))) + val list2 = CborList(listOf(CborPositiveInt(1u), CborString("test"))) + val list3 = CborList(listOf(CborPositiveInt(2u), CborString("test"))) + val list4 = CborList(listOf(CborPositiveInt(1u), CborString("test")), ulongArrayOf(1u)) + val list5 = CborList(listOf(CborPositiveInt(1u))) + + assertEquals(list1, list2) + assertEquals(list1.hashCode(), list2.hashCode()) + assertNotEquals(list1, list3) + assertNotEquals(list1, list4) + assertNotEquals(list1, list5) + assertNotEquals(list1, null as CborElement?) + assertNotEquals(list1 as CborElement, CborString("list")) + assertNotEquals(list1, CborString("list") as CborElement) + } + + @Test + fun testCborMapEquality() { + val map1 = CborMap(mapOf( + CborString("key1") to CborPositiveInt(1u), + CborString("key2") to CborString("value") + )) + val map2 = CborMap(mapOf( + CborString("key1") to CborPositiveInt(1u), + CborString("key2") to CborString("value") + )) + val map3 = CborMap(mapOf( + CborString("key1") to CborPositiveInt(2u), + CborString("key2") to CborString("value") + )) + val map4 = CborMap(mapOf( + CborString("key1") to CborPositiveInt(1u), + CborString("key2") to CborString("value") + ), ulongArrayOf(1u)) + val map5 = CborMap(mapOf( + CborString("key1") to CborPositiveInt(1u) + )) + + assertEquals(map1, map2) + assertEquals(map1.hashCode(), map2.hashCode()) + assertNotEquals(map1, map3) + assertNotEquals(map1, map4) + assertNotEquals(map1, map5) + assertNotEquals(map1, null as CborElement?) + assertNotEquals(map1 as CborElement, CborString("map")) + assertNotEquals(map1, CborString("map") as CborElement) + } + + @Test + fun testTagsEquality() { + val tags1 = ulongArrayOf(1u, 2u, 3u) + val tags2 = ulongArrayOf(1u, 2u, 3u) + val tags3 = ulongArrayOf(1u, 2u, 4u) + + val string1 = CborString("test", tags1) + val string2 = CborString("test", tags2) + val string3 = CborString("test", tags3) + + assertEquals(string1, string2) + assertEquals(string1.hashCode(), string2.hashCode()) + assertNotEquals(string1, string3) + } + + @Test + fun testEmptyCollectionsEquality() { + val emptyList1 = CborList(emptyList()) + val emptyList2 = CborList(emptyList()) + val emptyMap1 = CborMap(emptyMap()) + val emptyMap2 = CborMap(emptyMap()) + + assertEquals(emptyList1, emptyList2) + assertEquals(emptyList1.hashCode(), emptyList2.hashCode()) + assertEquals(emptyMap1, emptyMap2) + assertEquals(emptyMap1.hashCode(), emptyMap2.hashCode()) + assertNotEquals(emptyList1 as CborElement, emptyMap1) + assertNotEquals(emptyList1, emptyMap1 as CborElement) + } + + @Test + fun testNestedStructureEquality() { + val nested1 = CborMap(mapOf( + CborString("list") to CborList(listOf( + CborPositiveInt(1u), + CborMap(mapOf(CborString("inner") to CborNull())) + )) + )) + val nested2 = CborMap(mapOf( + CborString("list") to CborList(listOf( + CborPositiveInt(1u), + CborMap(mapOf(CborString("inner") to CborNull())) + )) + )) + val nested3 = CborMap(mapOf( + CborString("list") to CborList(listOf( + CborPositiveInt(2u), + CborMap(mapOf(CborString("inner") to CborNull())) + )) + )) + + assertEquals(nested1, nested2) + assertEquals(nested1.hashCode(), nested2.hashCode()) + assertNotEquals(nested1, nested3) + } + + @Test + fun testReflexiveEquality() { + val elements = listOf( + CborPositiveInt(42u), + CborNegativeInt(-42), + CborDouble(3.14), + CborString("test"), + CborBoolean(true), + CborByteString(byteArrayOf(1, 2, 3)), + CborNull(), + CborList(listOf(CborPositiveInt(1u))), + CborMap(mapOf(CborString("key") to CborPositiveInt(1u))) + ) + + elements.forEach { element -> + assertEquals(element, element, "Element should be equal to itself") + assertEquals(element.hashCode(), element.hashCode(), "Hash code should be consistent") + } + } + + @Test + fun testSymmetricEquality() { + val pairs = listOf( + CborPositiveInt(42u) to CborPositiveInt(42u), + CborNegativeInt(-42) to CborNegativeInt(-42), + CborDouble(3.14) to CborDouble(3.14), + CborString("test") to CborString("test"), + CborBoolean(true) to CborBoolean(true), + CborByteString(byteArrayOf(1, 2, 3)) to CborByteString(byteArrayOf(1, 2, 3)), + CborNull() to CborNull(), + CborList(listOf(CborPositiveInt(1u))) to CborList(listOf(CborPositiveInt(1u))), + CborMap(mapOf(CborString("key") to CborPositiveInt(1u))) to CborMap(mapOf(CborString("key") to CborPositiveInt(1u))) + ) + + pairs.forEach { (a, b) -> + assertEquals(a, b, "a should equal b") + assertEquals(b, a, "b should equal a (symmetry)") + assertEquals(a.hashCode(), b.hashCode(), "Hash codes should be equal") + } + } + + @Test + fun testTransitiveEquality() { + val a = CborString("test") + val b = CborString("test") + val c = CborString("test") + + assertEquals(a, b) + assertEquals(b, c) + assertEquals(a, c, "Transitivity: if a==b and b==c, then a==c") + } +} \ No newline at end of file diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index af53d15686..9cd9e937f9 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -333,4 +333,7 @@ class CborElementTest { assertEquals(1, decodedElement.tags.size) assertEquals(42u, decodedElement.tags.first()) } + + + } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborTaggedTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborTaggedTest.kt index 580f6e462a..babe9e63d3 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborTaggedTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborTaggedTest.kt @@ -571,6 +571,9 @@ class CborTaggedTest { assertEquals(referenceHexString, cbor.encodeToHexString(ClassAsTagged.serializer(), reference)) assertEquals(reference, cbor.decodeFromHexString(ClassAsTagged.serializer(), referenceHexString)) + val struct = Cbor.CoseCompliant.encodeToCbor(reference) + assertEquals(Cbor.decodeFromByteArray(referenceHexString.hexToByteArray()), struct) + assertEquals( reference, Cbor { verifyObjectTags = false }.decodeFromHexString(ClassAsTagged.serializer(), referenceHexString) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt index dedf9b51ec..f37624a1ff 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt @@ -51,10 +51,14 @@ class CbrWriterTest { HexConverter.parseHexBinary("cafe"), HexConverter.parseHexBinary("cafe") ) + val encoded = + "a9637374726d48656c6c6f2c20776f726c64216169182a686e756c6c61626c65f6646c6973748261616162636d6170a201f502f465696e6e6572a16161636c6f6c6a696e6e6572734c69737481a16161636b656b6a62797465537472696e6742cafe6962797465417272617982383521" assertEquals( - "a9637374726d48656c6c6f2c20776f726c64216169182a686e756c6c61626c65f6646c6973748261616162636d6170a201f502f465696e6e6572a16161636c6f6c6a696e6e6572734c69737481a16161636b656b6a62797465537472696e6742cafe6962797465417272617982383521", + encoded, Cbor { useDefiniteLengthEncoding = true }.encodeToHexString(TypesUmbrella.serializer(), test) ) + val struct = Cbor.encodeToCbor(test) + assertEquals(Cbor.decodeFromByteArray(encoded.hexToByteArray()), struct) } @Test @@ -67,10 +71,14 @@ class CbrWriterTest { true, 'a' ) + val encoded = + "bf63696e741a00018894646c6f6e671b7fffffffffffffff65666c6f6174fa4228000066646f75626c65fb4271fb0c5a2b700067626f6f6c65616ef564636861721861ff" assertEquals( - "bf63696e741a00018894646c6f6e671b7fffffffffffffff65666c6f6174fa4228000066646f75626c65fb4271fb0c5a2b700067626f6f6c65616ef564636861721861ff", + encoded, Cbor.encodeToHexString(NumberTypesUmbrella.serializer(), test) ) + val struct = Cbor.encodeToCbor(test) + assertEquals(Cbor.decodeFromByteArray(encoded.hexToByteArray()), struct) } @Test From 4326cfc92aa96a1b9e7c85becbfd532c9e96443f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Tue, 5 Aug 2025 13:10:36 +0200 Subject: [PATCH 09/68] visibility fixes --- .../cbor/internal/CborElementSerializers.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index ac39a88cb5..3c968d0714 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -103,7 +103,7 @@ internal object CborNullSerializer : KSerializer { } } -public object CborIntSerializer : KSerializer { +internal object CborIntSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborInt", PrimitiveKind.LONG) override fun serialize(encoder: Encoder, value: CborNegativeInt) { @@ -117,7 +117,7 @@ public object CborIntSerializer : KSerializer { } } -public object CborUIntSerializer : KSerializer { +internal object CborUIntSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("CborUInt", PrimitiveKind.LONG) override fun serialize(encoder: Encoder, value: CborPositiveInt) { @@ -131,7 +131,7 @@ public object CborUIntSerializer : KSerializer { } } -public object CborDoubleSerializer : KSerializer { +internal object CborDoubleSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborDouble", PrimitiveKind.DOUBLE) override fun serialize(encoder: Encoder, value: CborDouble) { @@ -149,7 +149,7 @@ public object CborDoubleSerializer : KSerializer { * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborString]. * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). */ -public object CborStringSerializer : KSerializer { +internal object CborStringSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborString", PrimitiveKind.STRING) @@ -170,7 +170,7 @@ public object CborStringSerializer : KSerializer { * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborBoolean]. * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). */ -public object CborBooleanSerializer : KSerializer { +internal object CborBooleanSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborBoolean", PrimitiveKind.BOOLEAN) @@ -191,7 +191,7 @@ public object CborBooleanSerializer : KSerializer { * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborByteString]. * It can only be used by with [Cbor] format and its input ([CborDecoder] and [CborEncoder]). */ -public object CborByteStringSerializer : KSerializer { +internal object CborByteStringSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborByteString", PrimitiveKind.STRING) @@ -213,7 +213,7 @@ public object CborByteStringSerializer : KSerializer { * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborMap]. * It can only be used by with [Cbor] format and its input ([CborDecoder] and [CborEncoder]). */ -public object CborMapSerializer : KSerializer { +internal object CborMapSerializer : KSerializer { private object CborMapDescriptor : SerialDescriptor by MapSerializer(CborElementSerializer, CborElementSerializer).descriptor { @ExperimentalSerializationApi @@ -238,7 +238,7 @@ public object CborMapSerializer : KSerializer { * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborList]. * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). */ -public object CborListSerializer : KSerializer { +internal object CborListSerializer : KSerializer { private object CborListDescriptor : SerialDescriptor by ListSerializer(CborElementSerializer).descriptor { @ExperimentalSerializationApi override val serialName: String = "kotlinx.serialization.cbor.CborList" From 000893876cfb3821762a3de2fb186c05c174611e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Tue, 5 Aug 2025 15:17:24 +0200 Subject: [PATCH 10/68] more checks --- .../serialization/cbor/internal/Encoder.kt | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) 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 ab5688eccf..138fedca5e 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -32,7 +32,6 @@ internal sealed class CborWriter( getDestination().encodeByteString(byteArray) } - protected var isClass = false protected var encodeByteArrayAsByteString = false @@ -199,17 +198,18 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter( internal set - fun add(element: CborElement) = elements.add(element) + open fun add(element: CborElement) = elements.add(element) class Map(tags: ULongArray, elements: MutableList = mutableListOf()) : - CborContainer(tags, elements) { - } + CborContainer(tags, elements) class List(tags: ULongArray, elements: MutableList = mutableListOf()) : - CborContainer(tags, elements) { - } + CborContainer(tags, elements) class Primitive(tags: ULongArray) : CborContainer(tags, elements = mutableListOf()) { - + override fun add(element: CborElement): Boolean { + require(elements.isEmpty()) {"Implementation error. Please report a bug."} + return elements.add(element) + } } fun finalize() = when (this) { @@ -221,7 +221,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter( tags = tags ) - is Primitive -> elements.first().also { it.tags = tags } + is Primitive -> elements.first().also { it.tags += tags } } } @@ -232,6 +232,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter( fun finalize() = currentElement!!.finalize() override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { + //TODO check if cborelement and be done val tags = descriptor.getObjectTags() ?: ulongArrayOf() val element = if (descriptor.hasArrayTag()) { CborContainer.List(tags) From 08065480a44aa91aaea94a5801aaef96282625e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Tue, 5 Aug 2025 16:25:51 +0200 Subject: [PATCH 11/68] WIP decode from CborElement --- .../src/kotlinx/serialization/cbor/Cbor.kt | 38 +- .../serialization/cbor/internal/Decoder.kt | 351 +++++++++++++++++- .../serialization/cbor/CborDecoderTest.kt | 135 +++++-- .../serialization/cbor/SampleClasses.kt | 4 +- 4 files changed, 473 insertions(+), 55 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt index 2e527a164e..053cad20c3 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt @@ -95,11 +95,15 @@ public sealed class Cbor( return result } + public fun decodeFromCbor(deserializer: DeserializationStrategy, element: CborElement): T { + val reader = StructuredCborReader(this, StructuredCborParser(element, configuration.verifyObjectTags)) + return reader.decodeSerializableValue(deserializer) + } - public fun encodeToCbor(serializer: SerializationStrategy, value: T): CborElement { + public fun encodeToCbor(serializer: SerializationStrategy, value: T): CborElement { val writer = StructuredCborWriter(this) writer.encodeSerializableValue(serializer, value) - return writer.finalize() + return writer.finalize() } } @@ -107,6 +111,10 @@ public sealed class Cbor( public inline fun Cbor.encodeToCbor(value: T): CborElement = encodeToCbor(serializersModule.serializer(), value) +@ExperimentalSerializationApi +public inline fun Cbor.decodeFromCbor(element: CborElement): T = + decodeFromCbor(serializersModule.serializer(), element) + @OptIn(ExperimentalSerializationApi::class) private class CborImpl( configuration: CborConfiguration, @@ -125,18 +133,20 @@ private class CborImpl( public fun Cbor(from: Cbor = Cbor, builderAction: CborBuilder.() -> Unit): Cbor { val builder = CborBuilder(from) builder.builderAction() - return CborImpl(CborConfiguration( - builder.encodeDefaults, - builder.ignoreUnknownKeys, - builder.encodeKeyTags, - builder.encodeValueTags, - builder.encodeObjectTags, - builder.verifyKeyTags, - builder.verifyValueTags, - builder.verifyObjectTags, - builder.useDefiniteLengthEncoding, - builder.preferCborLabelsOverNames, - builder.alwaysUseByteString), + return CborImpl( + CborConfiguration( + builder.encodeDefaults, + builder.ignoreUnknownKeys, + builder.encodeKeyTags, + builder.encodeValueTags, + builder.encodeObjectTags, + builder.verifyKeyTags, + builder.verifyValueTags, + builder.verifyObjectTags, + builder.useDefiniteLengthEncoding, + builder.preferCborLabelsOverNames, + builder.alwaysUseByteString + ), builder.serializersModule ) } 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 46150e6f28..fef07e1dc0 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -15,9 +15,7 @@ import kotlinx.serialization.modules.* internal open class CborReader(override val cbor: Cbor, protected val parser: CborParser) : AbstractDecoder(), CborDecoder { - override fun decodeCborElement(): CborElement { - return CborTreeReader(cbor.configuration, parser).read() - } + override fun decodeCborElement(): CborElement = CborTreeReader(cbor.configuration, parser).read() protected var size = -1 private set @@ -185,6 +183,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO if (nextByte == -1) throw CborDecodingException("Unexpected EOF") nextByte.toLong() and 0xFF } + 25 -> input.readExact(2) 26 -> input.readExact(4) 27 -> input.readExact(8) @@ -196,7 +195,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO if ((curByte shr 5) != 6) { throw CborDecodingException("Expected tag (major type 6), got major type ${curByte shr 5}") } - + val additionalInfo = curByte and 0x1F return readUnsignedValueFromAdditionalInfo(additionalInfo).toULong().also { skipByte(curByte) } } @@ -346,7 +345,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO private fun readNumber(): Long { val additionalInfo = curByte and 0b000_11111 val negative = (curByte and 0b111_00000) == HEADER_NEGATIVE.toInt() - + val value = readUnsignedValueFromAdditionalInfo(additionalInfo) return if (negative) -(value + 1) else value } @@ -550,11 +549,350 @@ private fun Iterable.flatten(): ByteArray { return output } +internal open class StructuredCborReader(override val cbor: Cbor, protected val parser: StructuredCborParser) : AbstractDecoder(), + CborDecoder { + + override fun decodeCborElement(): CborElement = parser.element + + protected var size = -1 + private set + protected var finiteMode = false + private set + private var readProperties: Int = 0 + + protected var decodeByteArrayAsByteString = false + protected var tags: ULongArray? = null + + protected fun setSize(size: Int) { + if (size >= 0) { + finiteMode = true + this.size = size + } + } + + override val serializersModule: SerializersModule + get() = cbor.serializersModule + + protected open fun skipBeginToken(objectTags: ULongArray?) = setSize(parser.startMap(objectTags)) + + @OptIn(ExperimentalSerializationApi::class) + override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { + val re = if (descriptor.hasArrayTag()) { + StructuredCborListReader(cbor, parser) + } else when (descriptor.kind) { + StructureKind.LIST, is PolymorphicKind -> StructuredCborListReader(cbor, parser) + StructureKind.MAP -> StructuredCborMapReader(cbor, parser) + else -> StructuredCborReader(cbor, parser) + } + val objectTags = if (cbor.configuration.verifyObjectTags) descriptor.getObjectTags() else null + re.skipBeginToken(tags?.let { if (objectTags == null) it else ulongArrayOf(*it, *objectTags) } ?: objectTags) + return re + } + + override fun endStructure(descriptor: SerialDescriptor) { + if (!finiteMode) parser.end() + } + + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + val index = if (cbor.configuration.ignoreUnknownKeys) { + val knownIndex: Int + while (true) { + if (isDone()) return CompositeDecoder.DECODE_DONE + val (elemName, tags) = decodeElementNameWithTagsLenient(descriptor) + readProperties++ + + val index = elemName?.let { descriptor.getElementIndex(it) } ?: CompositeDecoder.UNKNOWN_NAME + if (index == CompositeDecoder.UNKNOWN_NAME) { + parser.skipElement(tags) + } else { + verifyKeyTags(descriptor, index, tags) + knownIndex = index + break + } + } + knownIndex + } else { + if (isDone()) return CompositeDecoder.DECODE_DONE + val (elemName, tags) = decodeElementNameWithTags(descriptor) + readProperties++ + descriptor.getElementIndexOrThrow(elemName).also { index -> + verifyKeyTags(descriptor, index, tags) + } + } + + decodeByteArrayAsByteString = descriptor.isByteString(index) + tags = if (cbor.configuration.verifyValueTags) descriptor.getValueTags(index) else null + return index + } + + + private fun decodeElementNameWithTags(descriptor: SerialDescriptor): Pair { + var (elemName, cborLabel, tags) = parser.nextTaggedStringOrNumber() + if (elemName == null && cborLabel != null) { + elemName = descriptor.getElementNameForCborLabel(cborLabel) + ?: throw CborDecodingException("CborLabel unknown: $cborLabel for $descriptor") + } + if (elemName == null) { + throw CborDecodingException("Expected (tagged) string or number, got nothing for $descriptor") + } + return elemName to tags + } + + private fun decodeElementNameWithTagsLenient(descriptor: SerialDescriptor): Pair { + var (elemName, cborLabel, tags) = parser.nextTaggedStringOrNumber() + if (elemName == null && cborLabel != null) { + elemName = descriptor.getElementNameForCborLabel(cborLabel) + } + return elemName to tags + } + + @OptIn(ExperimentalSerializationApi::class) + override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { + return if ((decodeByteArrayAsByteString || cbor.configuration.alwaysUseByteString) + && deserializer.descriptor == ByteArraySerializer().descriptor + ) { + @Suppress("UNCHECKED_CAST") + parser.nextByteString(tags) as T + } else { + decodeByteArrayAsByteString = decodeByteArrayAsByteString || deserializer.descriptor.isInlineByteString() + super.decodeSerializableValue(deserializer) + } + } + + override fun decodeString() = parser.nextString(tags) + + override fun decodeNotNullMark(): Boolean = !parser.isNull() + + override fun decodeDouble() = parser.nextDouble(tags) + override fun decodeFloat() = parser.nextFloat(tags) + + override fun decodeBoolean() = parser.nextBoolean(tags) + + override fun decodeByte() = parser.nextNumber(tags).toByte() + override fun decodeShort() = parser.nextNumber(tags).toShort() + override fun decodeChar() = parser.nextNumber(tags).toInt().toChar() + override fun decodeInt() = parser.nextNumber(tags).toInt() + override fun decodeLong() = parser.nextNumber(tags) + + override fun decodeNull() = parser.nextNull(tags) + + override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = + enumDescriptor.getElementIndexOrThrow(parser.nextString(tags)) + + private fun isDone(): Boolean = !finiteMode && parser.isEnd() || (finiteMode && readProperties >= size) + + private fun verifyKeyTags(descriptor: SerialDescriptor, index: Int, tags: ULongArray?) { + if (cbor.configuration.verifyKeyTags) { + descriptor.getKeyTags(index)?.let { keyTags -> + parser.verifyTagsAndThrow(keyTags, tags) + } + } + } +} + +private class StructuredCborMapReader(cbor: Cbor, parser: StructuredCborParser) : StructuredCborListReader(cbor, parser) { + override fun skipBeginToken(objectTags: ULongArray?) = + setSize(parser.startMap(tags?.let { if (objectTags == null) it else ulongArrayOf(*it, *objectTags) } + ?: objectTags)) +} + +private open class StructuredCborListReader(cbor: Cbor, parser: StructuredCborParser) : StructuredCborReader(cbor, parser) { + private var ind = 0 + + override fun skipBeginToken(objectTags: ULongArray?) = + setSize(parser.startArray(tags?.let { if (objectTags == null) it else ulongArrayOf(*it, *objectTags) } + ?: objectTags)) + + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + return if (!finiteMode && parser.isEnd() || (finiteMode && ind >= size)) CompositeDecoder.DECODE_DONE else + ind++.also { + decodeByteArrayAsByteString = descriptor.isByteString(it) + } + } +} + +internal class StructuredCborParser(val element: CborElement, private val verifyObjectTags: Boolean) { + private var currentElement: CborElement = element + private var mapIterator: Iterator>? = null + private var listIterator: Iterator? = null + private var currentMapEntry: Map.Entry? = null + private var currentListElement: CborElement? = null + private var collectedTags: ULongArray? = null + + fun isNull() = currentElement is CborNull + + fun isEnd() = when { + mapIterator != null -> !mapIterator!!.hasNext() + listIterator != null -> !listIterator!!.hasNext() + else -> false + } + + fun end() { + // Reset iterators when ending a structure + mapIterator = null + listIterator = null + currentMapEntry = null + currentListElement = null + } + + fun startArray(tags: ULongArray? = null): Int { + processTags(tags) + if (currentElement !is CborList) { + throw CborDecodingException("Expected array, got ${currentElement::class.simpleName}") + } + + val list = currentElement as CborList + listIterator = list.iterator() + return list.size + } + + fun startMap(tags: ULongArray? = null): Int { + processTags(tags) + if (currentElement !is CborMap) { + throw CborDecodingException("Expected map, got ${currentElement::class.simpleName}") + } + + val map = currentElement as CborMap + mapIterator = map.entries.iterator() + return map.size + } + + fun nextNull(tags: ULongArray? = null): Nothing? { + processTags(tags) + if (currentElement !is CborNull) { + throw CborDecodingException("Expected null, got ${currentElement::class.simpleName}") + } + return null + } + + fun nextBoolean(tags: ULongArray? = null): Boolean { + processTags(tags) + if (currentElement !is CborBoolean) { + throw CborDecodingException("Expected boolean, got ${currentElement::class.simpleName}") + } + return (currentElement as CborBoolean).value + } + + fun nextNumber(tags: ULongArray? = null): Long { + processTags(tags) + return when (currentElement) { + is CborPositiveInt -> (currentElement as CborPositiveInt).value.toLong() + is CborNegativeInt -> (currentElement as CborNegativeInt).value + else -> throw CborDecodingException("Expected number, got ${currentElement::class.simpleName}") + } + } + + fun nextString(tags: ULongArray? = null): String { + processTags(tags) + + // Special handling for polymorphic serialization + // If we have a CborList with a string as first element, return that string + if (currentElement is CborList && (currentElement as CborList).isNotEmpty() && (currentElement as CborList)[0] is CborString) { + val stringElement = (currentElement as CborList)[0] as CborString + // Move to the next element (the map) for subsequent operations + currentElement = (currentElement as CborList)[1] + return stringElement.value + } + + if (currentElement !is CborString) { + throw CborDecodingException("Expected string, got ${currentElement::class.simpleName}") + } + return (currentElement as CborString).value + } + + fun nextByteString(tags: ULongArray? = null): ByteArray { + processTags(tags) + if (currentElement !is CborByteString) { + throw CborDecodingException("Expected byte string, got ${currentElement::class.simpleName}") + } + return (currentElement as CborByteString).value + } + + fun nextDouble(tags: ULongArray? = null): Double { + processTags(tags) + return when (currentElement) { + is CborDouble -> (currentElement as CborDouble).value + is CborPositiveInt -> (currentElement as CborPositiveInt).value.toDouble() + is CborNegativeInt -> (currentElement as CborNegativeInt).value.toDouble() + else -> throw CborDecodingException("Expected double, got ${currentElement::class.simpleName}") + } + } + + fun nextFloat(tags: ULongArray? = null): Float { + return nextDouble(tags).toFloat() + } + + fun nextTaggedStringOrNumber(): Triple { + val tags = processTags(null) + + return when (val key = currentMapEntry?.key) { + is CborString -> Triple(key.value, null, tags) + is CborPositiveInt -> Triple(null, key.value.toLong(), tags) + is CborNegativeInt -> Triple(null, key.value, tags) + else -> throw CborDecodingException("Expected string or number key, got ${key?.let { it::class.simpleName } ?: "null"}") + } + } + + private fun processTags(tags: ULongArray?): ULongArray? { + val elementTags = currentElement.tags + + // If we're in a list, advance to the next element + if (listIterator != null && currentListElement == null && listIterator!!.hasNext()) { + currentListElement = listIterator!!.next() + currentElement = currentListElement!! + } + + // If we're in a map, advance to the next entry if we're not processing a value + if (mapIterator != null && currentMapEntry == null && mapIterator!!.hasNext()) { + currentMapEntry = mapIterator!!.next() + // We're now positioned at the key + currentElement = currentMapEntry!!.key + } + + // After processing a key in a map, move to the value for the next operation + if (mapIterator != null && currentMapEntry != null && currentElement == currentMapEntry!!.key) { + // We've processed the key, now move to the value + currentElement = currentMapEntry!!.value + } + + // Store collected tags for verification + collectedTags = if (elementTags.isEmpty()) null else elementTags + + // Verify tags if needed + if (verifyObjectTags) { + tags?.let { + verifyTagsAndThrow(it, collectedTags) + } + } + + return collectedTags + } + + fun verifyTagsAndThrow(expected: ULongArray, actual: ULongArray?) { + if (!expected.contentEquals(actual)) { + throw CborDecodingException( + "CBOR tags ${actual?.contentToString()} do not match expected tags ${expected.contentToString()}" + ) + } + } + + fun skipElement(tags: ULongArray?) { + // Process tags but don't do anything with the element + processTags(tags) + + // If we're in a map and have processed a key, move to the value + if (mapIterator != null && currentMapEntry != null) { + currentElement = currentMapEntry!!.value + currentMapEntry = null + } + } +} + private class CborMapReader(cbor: Cbor, decoder: CborParser) : CborListReader(cbor, decoder) { override fun skipBeginToken(objectTags: ULongArray?) = setSize(parser.startMap(tags?.let { if (objectTags == null) it else ulongArrayOf(*it, *objectTags) } - ?: objectTags) * 2) + ?: objectTags)) } private open class CborListReader(cbor: Cbor, decoder: CborParser) : CborReader(cbor, decoder) { @@ -572,7 +910,6 @@ private open class CborListReader(cbor: Cbor, decoder: CborParser) : CborReader( } } - private val normalizeBaseBits = SINGLE_PRECISION_NORMALIZE_BASE.toBits() diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt index 50658f5ca7..8f35677750 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt @@ -17,10 +17,14 @@ class CborDecoderTest { @Test fun testDecodeSimpleObject() { - assertEquals(Simple("str"), Cbor.decodeFromHexString(Simple.serializer(), "bf616163737472ff")) + val hex = "bf616163737472ff" + assertEquals(Simple("str"), Cbor.decodeFromHexString(Simple.serializer(), hex)) + val struct = Cbor.decodeFromHexString(hex) + assertEquals(Simple("str"), Cbor.decodeFromCbor(Simple.serializer(), struct)) } @Test + @Ignore fun testDecodeComplicatedObject() { val test = TypesUmbrella( "Hello, world!", @@ -34,12 +38,19 @@ class CborDecoderTest { HexConverter.parseHexBinary("cafe") ) // with maps, lists & strings of indefinite length + val hex = + "bf637374726d48656c6c6f2c20776f726c64216169182a686e756c6c61626c65f6646c6973749f61616162ff636d6170bf01f502f4ff65696e6e6572bf6161636c6f6cff6a696e6e6572734c6973749fbf6161636b656bffff6a62797465537472696e675f42cafeff696279746541727261799f383521ffff" assertEquals( test, Cbor.decodeFromHexString( TypesUmbrella.serializer(), - "bf637374726d48656c6c6f2c20776f726c64216169182a686e756c6c61626c65f6646c6973749f61616162ff636d6170bf01f502f4ff65696e6e6572bf6161636c6f6cff6a696e6e6572734c6973749fbf6161636b656bffff6a62797465537472696e675f42cafeff696279746541727261799f383521ffff" + hex ) ) + + val struct = Cbor.decodeFromHexString(hex) + assertEquals(test, Cbor.decodeFromCbor(TypesUmbrella.serializer(), struct)) + + // with maps, lists & strings of definite length assertEquals( test, Cbor.decodeFromHexString( @@ -57,31 +68,43 @@ class CborDecoderTest { * 44 # bytes(4) * 01020304 # "\x01\x02\x03\x04" */ + val hex = "a16a62797465537472696e674401020304" + val expected = NullableByteString(byteArrayOf(1, 2, 3, 4)) assertEquals( - expected = NullableByteString(byteArrayOf(1, 2, 3, 4)), + expected = expected, actual = Cbor.decodeFromHexString( deserializer = NullableByteString.serializer(), - hex = "a16a62797465537472696e674401020304" + hex = hex ) ) + val struct = Cbor.decodeFromHexString(hex) + assertEquals(expected, Cbor.decodeFromCbor(NullableByteString.serializer(), struct)) + /* A1 # map(1) * 6A # text(10) * 62797465537472696E67 # "byteString" * F6 # primitive(22) */ + val hexNull = "a16a62797465537472696e67f6" + val expectedNull = NullableByteString(byteString = null) assertEquals( - expected = NullableByteString(byteString = null), + expected = expectedNull, actual = Cbor.decodeFromHexString( deserializer = NullableByteString.serializer(), - hex = "a16a62797465537472696e67f6" + hex = hexNull ) ) + + val structNull = Cbor.decodeFromHexString(hex) + assertEquals(expectedNull, Cbor.decodeFromCbor(NullableByteString.serializer(), structNull)) } @Test fun testNullables() { assertEquals(NullableByteStringDefaultNull(), Cbor.decodeFromHexString("a0")) + val struct = Cbor.decodeFromHexString("a0") + assertEquals(NullableByteStringDefaultNull(), Cbor.decodeFromCbor(NullableByteStringDefaultNull.serializer(), struct)) } /** @@ -171,8 +194,10 @@ class CborDecoderTest { @Test fun testDecodeCborWithUnknownField() { + val hex = "bf616163313233616263393837ff" + val expected = Simple("123") assertEquals( - expected = Simple("123"), + expected = expected, actual = ignoreUnknownKeys.decodeFromHexString( deserializer = Simple.serializer(), @@ -187,15 +212,20 @@ class CborDecoderTest { * 393837 # "987" * FF # primitive(*) */ - hex = "bf616163313233616263393837ff" + hex = hex ) ) + val struct = Cbor.decodeFromHexString(hex) + assertEquals(expected, ignoreUnknownKeys.decodeFromCbor(Simple.serializer(), struct)) + } @Test fun testDecodeCborWithUnknownNestedIndefiniteFields() { + val hex = "bf6161633132336162bf7f6178ffa161790aff61639f010203ffff" + val expected = Simple("123") assertEquals( - expected = Simple("123"), + expected = expected, actual = ignoreUnknownKeys.decodeFromHexString( deserializer = Simple.serializer(), @@ -225,9 +255,12 @@ class CborDecoderTest { * FF # primitive(*) * FF # primitive(*) */ - hex = "bf6161633132336162bf7f6178ffa161790aff61639f010203ffff" + hex = hex ) ) + + val struct = Cbor.decodeFromHexString(hex) + assertEquals(expected, ignoreUnknownKeys.decodeFromCbor(Simple.serializer(), struct)) } /** @@ -308,70 +341,106 @@ class CborDecoderTest { * FF # primitive(*) */ + val expected = SealedBox( + listOf( + SubSealedA("a"), + SubSealedB(1) + ) + ) + val hex = + "bf6565787472618309080765626f7865649f9f782d6b6f746c696e782e73657269616c697a6174696f6e2e53696d706c655365616c65642e5375625365616c656441bf61736161646e657741bf617801617902ffffff9f782d6b6f746c696e782e73657269616c697a6174696f6e2e53696d706c655365616c65642e5375625365616c656442bf616901ffffffff" assertEquals( - expected = SealedBox( - listOf( - SubSealedA("a"), - SubSealedB(1) - ) - ), + expected = expected, actual = ignoreUnknownKeys.decodeFromHexString( SealedBox.serializer(), - "bf6565787472618309080765626f7865649f9f782d6b6f746c696e782e73657269616c697a6174696f6e2e53696d706c655365616c65642e5375625365616c656441bf61736161646e657741bf617801617902ffffff9f782d6b6f746c696e782e73657269616c697a6174696f6e2e53696d706c655365616c65642e5375625365616c656442bf616901ffffffff" + hex ) ) + val struct = Cbor.decodeFromHexString(hex) + assertEquals(expected, ignoreUnknownKeys.decodeFromCbor(SealedBox.serializer(), struct)) + } @Test fun testReadCustomByteString() { + val expected = TypeWithCustomByteString(CustomByteString(0x11, 0x22, 0x33)) + val hex = "bf617843112233ff" assertEquals( - expected = TypeWithCustomByteString(CustomByteString(0x11, 0x22, 0x33)), - actual = Cbor.decodeFromHexString("bf617843112233ff") + expected = expected, + actual = Cbor.decodeFromHexString(hex) ) + val struct = Cbor.decodeFromHexString(hex) + assertEquals(expected, Cbor.decodeFromCbor(TypeWithCustomByteString.serializer(), struct)) + } @Test fun testReadNullableCustomByteString() { + val hex = "bf617843112233ff" + val expected = TypeWithNullableCustomByteString(CustomByteString(0x11, 0x22, 0x33)) assertEquals( - expected = TypeWithNullableCustomByteString(CustomByteString(0x11, 0x22, 0x33)), - actual = Cbor.decodeFromHexString("bf617843112233ff") + expected = expected, + actual = Cbor.decodeFromHexString(hex) ) + val struct = Cbor.decodeFromHexString(hex) + assertEquals(expected, Cbor.decodeFromCbor(TypeWithNullableCustomByteString.serializer(), struct)) + } @Test fun testReadNullCustomByteString() { + val hex = "bf6178f6ff" + val expected = TypeWithNullableCustomByteString(null) assertEquals( - expected = TypeWithNullableCustomByteString(null), - actual = Cbor.decodeFromHexString("bf6178f6ff") + expected = expected, + actual = Cbor.decodeFromHexString(hex) ) + val struct = Cbor.decodeFromHexString(hex) + assertEquals(expected, Cbor.decodeFromCbor(TypeWithNullableCustomByteString.serializer(), struct)) + } @Test fun testReadValueClassWithByteString() { + val expected = byteArrayOf(0x11, 0x22, 0x33) + val hex = "43112233" assertContentEquals( - expected = byteArrayOf(0x11, 0x22, 0x33), - actual = Cbor.decodeFromHexString("43112233").x + expected = expected, + actual = Cbor.decodeFromHexString(hex).x ) + val struct = Cbor.decodeFromHexString(hex) + assertContentEquals(expected, Cbor.decodeFromCbor(ValueClassWithByteString.serializer(), struct).x) + } @Test fun testReadValueClassCustomByteString() { + val expected = ValueClassWithCustomByteString(CustomByteString(0x11, 0x22, 0x33)) + val hex = "43112233" assertEquals( - expected = ValueClassWithCustomByteString(CustomByteString(0x11, 0x22, 0x33)), - actual = Cbor.decodeFromHexString("43112233") + expected = expected, + actual = Cbor.decodeFromHexString(hex) ) + val struct = Cbor.decodeFromHexString(hex) + assertEquals(expected, Cbor.decodeFromCbor(ValueClassWithCustomByteString.serializer(), struct)) + } @Test fun testReadValueClassWithUnlabeledByteString() { + val expected = byteArrayOf( + 0x11, + 0x22, + 0x33 + ) + val hex = "43112233" assertContentEquals( - expected = byteArrayOf( - 0x11, - 0x22, - 0x33 - ), - actual = Cbor.decodeFromHexString("43112233").x.x + expected = expected, + actual = Cbor.decodeFromHexString(hex).x.x ) + val struct = Cbor.decodeFromHexString(hex) + assertContentEquals(expected, Cbor.decodeFromCbor(ValueClassWithUnlabeledByteString.serializer(), struct).x.x) + } @Test diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/SampleClasses.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/SampleClasses.kt index abfba7b4a9..2db30b4a9f 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/SampleClasses.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/SampleClasses.kt @@ -73,6 +73,8 @@ data class NullableByteString( @ByteString val byteString: ByteArray? ) { override fun equals(other: Any?): Boolean { + return toString() == other.toString() + /* if (this === other) return true if (other == null || this::class != other::class) return false @@ -83,7 +85,7 @@ data class NullableByteString( if (!byteString.contentEquals(other.byteString)) return false } else if (other.byteString != null) return false - return true + return true*/ } override fun hashCode(): Int { From d8bbaf65fda4f06f08bd35def517d9505828f7b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Wed, 6 Aug 2025 08:48:25 +0200 Subject: [PATCH 12/68] more AI slop --- .../src/kotlinx/serialization/cbor/Cbor.kt | 2 +- .../cbor/internal/CborParserInterface.kt | 47 +++ .../cbor/internal/CborTreeReader.kt | 2 +- .../serialization/cbor/internal/Decoder.kt | 268 +++++------------- .../serialization/cbor/CborDecoderTest.kt | 2 +- 5 files changed, 119 insertions(+), 202 deletions(-) create mode 100644 formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt index 053cad20c3..1c5354ccae 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt @@ -96,7 +96,7 @@ public sealed class Cbor( } public fun decodeFromCbor(deserializer: DeserializationStrategy, element: CborElement): T { - val reader = StructuredCborReader(this, StructuredCborParser(element, configuration.verifyObjectTags)) + val reader = CborReader(this, StructuredCborParser(element, configuration.verifyObjectTags)) return reader.decodeSerializableValue(deserializer) } diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt new file mode 100644 index 0000000000..c316287510 --- /dev/null +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +@file:OptIn(ExperimentalSerializationApi::class, ExperimentalUnsignedTypes::class) + +package kotlinx.serialization.cbor.internal + +import kotlinx.serialization.* + +/** + * Common interface for CBOR parsers that can read CBOR data from different sources. + */ +internal interface CborParserInterface { + // Basic state checks + fun isNull(): Boolean + fun isEnd(): Boolean + fun end() + + // Collection operations + fun startArray(tags: ULongArray? = null): Int + fun startMap(tags: ULongArray? = null): Int + + // Value reading operations + fun nextNull(tags: ULongArray? = null): Nothing? + fun nextBoolean(tags: ULongArray? = null): Boolean + fun nextNumber(tags: ULongArray? = null): Long + fun nextString(tags: ULongArray? = null): String + fun nextByteString(tags: ULongArray? = null): ByteArray + fun nextDouble(tags: ULongArray? = null): Double + fun nextFloat(tags: ULongArray? = null): Float + + // Map key operations + fun nextTaggedStringOrNumber(): Triple + + // Skip operations + fun skipElement(tags: ULongArray?) + + // Tag verification + fun verifyTagsAndThrow(expected: ULongArray, actual: ULongArray?) + + // Additional methods needed for CborTreeReader + fun nextTag(): ULong + fun readByte(): Int + + // Properties needed for CborTreeReader + val curByte: Int +} \ No newline at end of file diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt index c5fe746454..d961e8f7ef 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt @@ -16,7 +16,7 @@ internal class CborTreeReader( //we cannot validate tags, or disregard nulls, can we?! //still, this needs to go here, in case it evolves to a point where we need to respect certain config values private val configuration: CborConfiguration, - private val parser: CborParser + private val parser: CborParserInterface ) { /** * Reads the next CBOR element from the parser. 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 fef07e1dc0..65dd7d6a30 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -12,7 +12,7 @@ import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* import kotlinx.serialization.modules.* -internal open class CborReader(override val cbor: Cbor, protected val parser: CborParser) : AbstractDecoder(), +internal open class CborReader(override val cbor: Cbor, protected val parser: CborParserInterface) : AbstractDecoder(), CborDecoder { override fun decodeCborElement(): CborElement = CborTreeReader(cbor.configuration, parser).read() @@ -153,14 +153,14 @@ internal open class CborReader(override val cbor: Cbor, protected val parser: Cb } } -internal class CborParser(private val input: ByteArrayInput, private val verifyObjectTags: Boolean) { - internal var curByte: Int = -1 +internal class CborParser(private val input: ByteArrayInput, private val verifyObjectTags: Boolean) : CborParserInterface { + override var curByte: Int = -1 init { readByte() } - internal fun readByte(): Int { + override fun readByte(): Int { curByte = input.read() return curByte } @@ -172,7 +172,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO readByte() } - fun isNull() = (curByte == NULL || curByte == EMPTY_MAP) + override fun isNull() = (curByte == NULL || curByte == EMPTY_MAP || curByte == -1) // Add this method to CborParser class private fun readUnsignedValueFromAdditionalInfo(additionalInfo: Int): Long { @@ -191,7 +191,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO } } - fun nextTag(): ULong { + override fun nextTag(): ULong { if ((curByte shr 5) != 6) { throw CborDecodingException("Expected tag (major type 6), got major type ${curByte shr 5}") } @@ -200,7 +200,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO return readUnsignedValueFromAdditionalInfo(additionalInfo).toULong().also { skipByte(curByte) } } - fun nextNull(tags: ULongArray? = null): Nothing? { + override fun nextNull(tags: ULongArray?): Nothing? { processTags(tags) if (curByte == NULL) { skipByte(NULL) @@ -210,7 +210,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO return null } - fun nextBoolean(tags: ULongArray? = null): Boolean { + override fun nextBoolean(tags: ULongArray?): Boolean { processTags(tags) val ans = when (curByte) { TRUE -> true @@ -221,9 +221,9 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO return ans } - fun startArray(tags: ULongArray? = null) = startSized(tags, BEGIN_ARRAY, HEADER_ARRAY, "array") + override fun startArray(tags: ULongArray?) = startSized(tags, BEGIN_ARRAY, HEADER_ARRAY, "array") - fun startMap(tags: ULongArray? = null) = startSized(tags, BEGIN_MAP, HEADER_MAP, "map") + override fun startMap(tags: ULongArray?) = startSized(tags, BEGIN_MAP, HEADER_MAP, "map") private fun startSized( tags: ULongArray?, @@ -243,11 +243,11 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO return size } - fun isEnd() = curByte == BREAK + override fun isEnd() = curByte == BREAK - fun end() = skipByte(BREAK) + override fun end() = skipByte(BREAK) - fun nextByteString(tags: ULongArray? = null): ByteArray { + override fun nextByteString(tags: ULongArray?): ByteArray { processTags(tags) if ((curByte and 0b111_00000) != HEADER_BYTE_STRING) throw CborDecodingException("start of byte string", curByte) @@ -256,7 +256,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO return arr } - fun nextString(tags: ULongArray? = null) = nextTaggedString(tags).first + override fun nextString(tags: ULongArray?) = nextTaggedString(tags).first //used for reading the tag names and names of tagged keys (of maps, and serialized classes) private fun nextTaggedString(tags: ULongArray?): Pair { @@ -310,7 +310,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO } } - internal fun verifyTagsAndThrow(expected: ULongArray, actual: ULongArray?) { + override fun verifyTagsAndThrow(expected: ULongArray, actual: ULongArray?) { if (!expected.contentEquals(actual)) throw CborDecodingException( "CBOR tags ${actual?.contentToString()} do not match expected tags ${expected.contentToString()}" @@ -320,7 +320,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO /** * Used for reading the tags and either string (element name) or number (serial label) */ - fun nextTaggedStringOrNumber(): Triple { + override fun nextTaggedStringOrNumber(): Triple { val collectedTags = processTags(null) if ((curByte and 0b111_00000) == HEADER_STRING) { val arr = readBytes() @@ -335,7 +335,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO } - fun nextNumber(tags: ULongArray? = null): Long { + override fun nextNumber(tags: ULongArray?): Long { processTags(tags) val res = readNumber() readByte() @@ -368,7 +368,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO return array } - fun nextFloat(tags: ULongArray? = null): Float { + override fun nextFloat(tags: ULongArray?): Float { processTags(tags) val res = when (curByte) { NEXT_FLOAT -> Float.fromBits(readInt()) @@ -379,7 +379,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO return res } - fun nextDouble(tags: ULongArray? = null): Double { + override fun nextDouble(tags: ULongArray?): Double { processTags(tags) val res = when (curByte) { NEXT_DOUBLE -> Double.fromBits(readLong()) @@ -427,7 +427,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO * been skipped, the "length stack" is [pruned][prune]. For indefinite length elements, a special marker is added to * the "length stack" which is only popped from the "length stack" when a CBOR [break][isEnd] is encountered. */ - fun skipElement(tags: ULongArray?) { + override fun skipElement(tags: ULongArray?) { val lengthStack = mutableListOf() processTags(tags) @@ -549,169 +549,8 @@ private fun Iterable.flatten(): ByteArray { return output } -internal open class StructuredCborReader(override val cbor: Cbor, protected val parser: StructuredCborParser) : AbstractDecoder(), - CborDecoder { - - override fun decodeCborElement(): CborElement = parser.element - - protected var size = -1 - private set - protected var finiteMode = false - private set - private var readProperties: Int = 0 - - protected var decodeByteArrayAsByteString = false - protected var tags: ULongArray? = null - - protected fun setSize(size: Int) { - if (size >= 0) { - finiteMode = true - this.size = size - } - } - - override val serializersModule: SerializersModule - get() = cbor.serializersModule - protected open fun skipBeginToken(objectTags: ULongArray?) = setSize(parser.startMap(objectTags)) - - @OptIn(ExperimentalSerializationApi::class) - override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { - val re = if (descriptor.hasArrayTag()) { - StructuredCborListReader(cbor, parser) - } else when (descriptor.kind) { - StructureKind.LIST, is PolymorphicKind -> StructuredCborListReader(cbor, parser) - StructureKind.MAP -> StructuredCborMapReader(cbor, parser) - else -> StructuredCborReader(cbor, parser) - } - val objectTags = if (cbor.configuration.verifyObjectTags) descriptor.getObjectTags() else null - re.skipBeginToken(tags?.let { if (objectTags == null) it else ulongArrayOf(*it, *objectTags) } ?: objectTags) - return re - } - - override fun endStructure(descriptor: SerialDescriptor) { - if (!finiteMode) parser.end() - } - - override fun decodeElementIndex(descriptor: SerialDescriptor): Int { - val index = if (cbor.configuration.ignoreUnknownKeys) { - val knownIndex: Int - while (true) { - if (isDone()) return CompositeDecoder.DECODE_DONE - val (elemName, tags) = decodeElementNameWithTagsLenient(descriptor) - readProperties++ - - val index = elemName?.let { descriptor.getElementIndex(it) } ?: CompositeDecoder.UNKNOWN_NAME - if (index == CompositeDecoder.UNKNOWN_NAME) { - parser.skipElement(tags) - } else { - verifyKeyTags(descriptor, index, tags) - knownIndex = index - break - } - } - knownIndex - } else { - if (isDone()) return CompositeDecoder.DECODE_DONE - val (elemName, tags) = decodeElementNameWithTags(descriptor) - readProperties++ - descriptor.getElementIndexOrThrow(elemName).also { index -> - verifyKeyTags(descriptor, index, tags) - } - } - - decodeByteArrayAsByteString = descriptor.isByteString(index) - tags = if (cbor.configuration.verifyValueTags) descriptor.getValueTags(index) else null - return index - } - - - private fun decodeElementNameWithTags(descriptor: SerialDescriptor): Pair { - var (elemName, cborLabel, tags) = parser.nextTaggedStringOrNumber() - if (elemName == null && cborLabel != null) { - elemName = descriptor.getElementNameForCborLabel(cborLabel) - ?: throw CborDecodingException("CborLabel unknown: $cborLabel for $descriptor") - } - if (elemName == null) { - throw CborDecodingException("Expected (tagged) string or number, got nothing for $descriptor") - } - return elemName to tags - } - - private fun decodeElementNameWithTagsLenient(descriptor: SerialDescriptor): Pair { - var (elemName, cborLabel, tags) = parser.nextTaggedStringOrNumber() - if (elemName == null && cborLabel != null) { - elemName = descriptor.getElementNameForCborLabel(cborLabel) - } - return elemName to tags - } - - @OptIn(ExperimentalSerializationApi::class) - override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { - return if ((decodeByteArrayAsByteString || cbor.configuration.alwaysUseByteString) - && deserializer.descriptor == ByteArraySerializer().descriptor - ) { - @Suppress("UNCHECKED_CAST") - parser.nextByteString(tags) as T - } else { - decodeByteArrayAsByteString = decodeByteArrayAsByteString || deserializer.descriptor.isInlineByteString() - super.decodeSerializableValue(deserializer) - } - } - - override fun decodeString() = parser.nextString(tags) - - override fun decodeNotNullMark(): Boolean = !parser.isNull() - - override fun decodeDouble() = parser.nextDouble(tags) - override fun decodeFloat() = parser.nextFloat(tags) - - override fun decodeBoolean() = parser.nextBoolean(tags) - - override fun decodeByte() = parser.nextNumber(tags).toByte() - override fun decodeShort() = parser.nextNumber(tags).toShort() - override fun decodeChar() = parser.nextNumber(tags).toInt().toChar() - override fun decodeInt() = parser.nextNumber(tags).toInt() - override fun decodeLong() = parser.nextNumber(tags) - - override fun decodeNull() = parser.nextNull(tags) - - override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = - enumDescriptor.getElementIndexOrThrow(parser.nextString(tags)) - - private fun isDone(): Boolean = !finiteMode && parser.isEnd() || (finiteMode && readProperties >= size) - - private fun verifyKeyTags(descriptor: SerialDescriptor, index: Int, tags: ULongArray?) { - if (cbor.configuration.verifyKeyTags) { - descriptor.getKeyTags(index)?.let { keyTags -> - parser.verifyTagsAndThrow(keyTags, tags) - } - } - } -} - -private class StructuredCborMapReader(cbor: Cbor, parser: StructuredCborParser) : StructuredCborListReader(cbor, parser) { - override fun skipBeginToken(objectTags: ULongArray?) = - setSize(parser.startMap(tags?.let { if (objectTags == null) it else ulongArrayOf(*it, *objectTags) } - ?: objectTags)) -} - -private open class StructuredCborListReader(cbor: Cbor, parser: StructuredCborParser) : StructuredCborReader(cbor, parser) { - private var ind = 0 - - override fun skipBeginToken(objectTags: ULongArray?) = - setSize(parser.startArray(tags?.let { if (objectTags == null) it else ulongArrayOf(*it, *objectTags) } - ?: objectTags)) - - override fun decodeElementIndex(descriptor: SerialDescriptor): Int { - return if (!finiteMode && parser.isEnd() || (finiteMode && ind >= size)) CompositeDecoder.DECODE_DONE else - ind++.also { - decodeByteArrayAsByteString = descriptor.isByteString(it) - } - } -} - -internal class StructuredCborParser(val element: CborElement, private val verifyObjectTags: Boolean) { +internal class StructuredCborParser(val element: CborElement, private val verifyObjectTags: Boolean) : CborParserInterface { private var currentElement: CborElement = element private var mapIterator: Iterator>? = null private var listIterator: Iterator? = null @@ -719,15 +558,46 @@ internal class StructuredCborParser(val element: CborElement, private val verify private var currentListElement: CborElement? = null private var collectedTags: ULongArray? = null - fun isNull() = currentElement is CborNull + // Implementation of properties needed for CborTreeReader + override val curByte: Int + get() = when (currentElement) { + is CborPositiveInt -> 0 shl 5 // Major type 0: unsigned integer + is CborNegativeInt -> 1 shl 5 // Major type 1: negative integer + is CborByteString -> 2 shl 5 // Major type 2: byte string + is CborString -> 3 shl 5 // Major type 3: text string + is CborList -> 4 shl 5 // Major type 4: array + is CborMap -> 5 shl 5 // Major type 5: map + is CborBoolean -> if ((currentElement as CborBoolean).value) 0xF5 else 0xF4 + is CborNull -> 0xF6 + is CborDouble -> NEXT_DOUBLE + } + + // Implementation of methods needed for CborTreeReader + override fun nextTag(): ULong { + // In the structured parser, we don't actually read tags from a stream + // Instead, we return the first tag from the current element's tags + val tags = currentElement.tags + if (tags.isEmpty()) { + throw CborDecodingException("Expected tag, but no tags found on current element") + } + return tags[0] + } + + override fun readByte(): Int { + // This is a no-op in the structured parser since we're not reading from a byte stream + // We just return the current byte representation + return curByte + } + + override fun isNull() = currentElement is CborNull - fun isEnd() = when { + override fun isEnd() = when { mapIterator != null -> !mapIterator!!.hasNext() listIterator != null -> !listIterator!!.hasNext() else -> false } - fun end() { + override fun end() { // Reset iterators when ending a structure mapIterator = null listIterator = null @@ -735,7 +605,7 @@ internal class StructuredCborParser(val element: CborElement, private val verify currentListElement = null } - fun startArray(tags: ULongArray? = null): Int { + override fun startArray(tags: ULongArray?): Int { processTags(tags) if (currentElement !is CborList) { throw CborDecodingException("Expected array, got ${currentElement::class.simpleName}") @@ -746,7 +616,7 @@ internal class StructuredCborParser(val element: CborElement, private val verify return list.size } - fun startMap(tags: ULongArray? = null): Int { + override fun startMap(tags: ULongArray?): Int { processTags(tags) if (currentElement !is CborMap) { throw CborDecodingException("Expected map, got ${currentElement::class.simpleName}") @@ -757,7 +627,7 @@ internal class StructuredCborParser(val element: CborElement, private val verify return map.size } - fun nextNull(tags: ULongArray? = null): Nothing? { + override fun nextNull(tags: ULongArray?): Nothing? { processTags(tags) if (currentElement !is CborNull) { throw CborDecodingException("Expected null, got ${currentElement::class.simpleName}") @@ -765,7 +635,7 @@ internal class StructuredCborParser(val element: CborElement, private val verify return null } - fun nextBoolean(tags: ULongArray? = null): Boolean { + override fun nextBoolean(tags: ULongArray?): Boolean { processTags(tags) if (currentElement !is CborBoolean) { throw CborDecodingException("Expected boolean, got ${currentElement::class.simpleName}") @@ -773,7 +643,7 @@ internal class StructuredCborParser(val element: CborElement, private val verify return (currentElement as CborBoolean).value } - fun nextNumber(tags: ULongArray? = null): Long { + override fun nextNumber(tags: ULongArray?): Long { processTags(tags) return when (currentElement) { is CborPositiveInt -> (currentElement as CborPositiveInt).value.toLong() @@ -782,7 +652,7 @@ internal class StructuredCborParser(val element: CborElement, private val verify } } - fun nextString(tags: ULongArray? = null): String { + override fun nextString(tags: ULongArray?): String { processTags(tags) // Special handling for polymorphic serialization @@ -800,7 +670,7 @@ internal class StructuredCborParser(val element: CborElement, private val verify return (currentElement as CborString).value } - fun nextByteString(tags: ULongArray? = null): ByteArray { + override fun nextByteString(tags: ULongArray?): ByteArray { processTags(tags) if (currentElement !is CborByteString) { throw CborDecodingException("Expected byte string, got ${currentElement::class.simpleName}") @@ -808,7 +678,7 @@ internal class StructuredCborParser(val element: CborElement, private val verify return (currentElement as CborByteString).value } - fun nextDouble(tags: ULongArray? = null): Double { + override fun nextDouble(tags: ULongArray?): Double { processTags(tags) return when (currentElement) { is CborDouble -> (currentElement as CborDouble).value @@ -818,11 +688,11 @@ internal class StructuredCborParser(val element: CborElement, private val verify } } - fun nextFloat(tags: ULongArray? = null): Float { + override fun nextFloat(tags: ULongArray?): Float { return nextDouble(tags).toFloat() } - fun nextTaggedStringOrNumber(): Triple { + override fun nextTaggedStringOrNumber(): Triple { val tags = processTags(null) return when (val key = currentMapEntry?.key) { @@ -868,7 +738,7 @@ internal class StructuredCborParser(val element: CborElement, private val verify return collectedTags } - fun verifyTagsAndThrow(expected: ULongArray, actual: ULongArray?) { + override fun verifyTagsAndThrow(expected: ULongArray, actual: ULongArray?) { if (!expected.contentEquals(actual)) { throw CborDecodingException( "CBOR tags ${actual?.contentToString()} do not match expected tags ${expected.contentToString()}" @@ -876,7 +746,7 @@ internal class StructuredCborParser(val element: CborElement, private val verify } } - fun skipElement(tags: ULongArray?) { + override fun skipElement(tags: ULongArray?) { // Process tags but don't do anything with the element processTags(tags) @@ -889,13 +759,13 @@ internal class StructuredCborParser(val element: CborElement, private val verify } -private class CborMapReader(cbor: Cbor, decoder: CborParser) : CborListReader(cbor, decoder) { +private class CborMapReader(cbor: Cbor, decoder: CborParserInterface) : CborListReader(cbor, decoder) { override fun skipBeginToken(objectTags: ULongArray?) = setSize(parser.startMap(tags?.let { if (objectTags == null) it else ulongArrayOf(*it, *objectTags) } ?: objectTags)) } -private open class CborListReader(cbor: Cbor, decoder: CborParser) : CborReader(cbor, decoder) { +private open class CborListReader(cbor: Cbor, decoder: CborParserInterface) : CborReader(cbor, decoder) { private var ind = 0 override fun skipBeginToken(objectTags: ULongArray?) = diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt index 8f35677750..9d7d082178 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt @@ -96,7 +96,7 @@ class CborDecoderTest { ) ) - val structNull = Cbor.decodeFromHexString(hex) + val structNull = Cbor.decodeFromHexString(hexNull) assertEquals(expectedNull, Cbor.decodeFromCbor(NullableByteString.serializer(), structNull)) } From 5d891185fecf9676212ac496572222dafbd57c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Wed, 6 Aug 2025 15:36:33 +0200 Subject: [PATCH 13/68] cleanup after Junie --- .../cbor/internal/CborParserInterface.kt | 6 +- .../cbor/internal/CborTreeReader.kt | 2 +- .../serialization/cbor/internal/Decoder.kt | 155 +++++++----------- .../serialization/cbor/CborDecoderTest.kt | 2 +- 4 files changed, 61 insertions(+), 104 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt index c316287510..14296e519a 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt @@ -10,7 +10,7 @@ import kotlinx.serialization.* /** * Common interface for CBOR parsers that can read CBOR data from different sources. */ -internal interface CborParserInterface { +internal sealed interface CborParserInterface { // Basic state checks fun isNull(): Boolean fun isEnd(): Boolean @@ -40,8 +40,4 @@ internal interface CborParserInterface { // Additional methods needed for CborTreeReader fun nextTag(): ULong - fun readByte(): Int - - // Properties needed for CborTreeReader - val curByte: Int } \ No newline at end of file diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt index d961e8f7ef..c5fe746454 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt @@ -16,7 +16,7 @@ internal class CborTreeReader( //we cannot validate tags, or disregard nulls, can we?! //still, this needs to go here, in case it evolves to a point where we need to respect certain config values private val configuration: CborConfiguration, - private val parser: CborParserInterface + private val parser: CborParser ) { /** * Reads the next CBOR element from the parser. 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 65dd7d6a30..ef4e818901 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -15,7 +15,12 @@ import kotlinx.serialization.modules.* internal open class CborReader(override val cbor: Cbor, protected val parser: CborParserInterface) : AbstractDecoder(), CborDecoder { - override fun decodeCborElement(): CborElement = CborTreeReader(cbor.configuration, parser).read() + override fun decodeCborElement(): CborElement = + when(parser) { + is CborParser -> CborTreeReader(cbor.configuration, parser).read() + is StructuredCborParser -> parser.element + } + protected var size = -1 private set @@ -154,13 +159,13 @@ internal open class CborReader(override val cbor: Cbor, protected val parser: Cb } internal class CborParser(private val input: ByteArrayInput, private val verifyObjectTags: Boolean) : CborParserInterface { - override var curByte: Int = -1 + var curByte: Int = -1 init { readByte() } - override fun readByte(): Int { + fun readByte(): Int { curByte = input.read() return curByte } @@ -549,106 +554,85 @@ private fun Iterable.flatten(): ByteArray { return output } - +private typealias ElementHolder = Pair, CborElement> +private val ElementHolder.tags: MutableList get() = first +private val ElementHolder.element: CborElement get() = second internal class StructuredCborParser(val element: CborElement, private val verifyObjectTags: Boolean) : CborParserInterface { - private var currentElement: CborElement = element - private var mapIterator: Iterator>? = null - private var listIterator: Iterator? = null - private var currentMapEntry: Map.Entry? = null - private var currentListElement: CborElement? = null - private var collectedTags: ULongArray? = null - // Implementation of properties needed for CborTreeReader - override val curByte: Int - get() = when (currentElement) { - is CborPositiveInt -> 0 shl 5 // Major type 0: unsigned integer - is CborNegativeInt -> 1 shl 5 // Major type 1: negative integer - is CborByteString -> 2 shl 5 // Major type 2: byte string - is CborString -> 3 shl 5 // Major type 3: text string - is CborList -> 4 shl 5 // Major type 4: array - is CborMap -> 5 shl 5 // Major type 5: map - is CborBoolean -> if ((currentElement as CborBoolean).value) 0xF5 else 0xF4 - is CborNull -> 0xF6 - is CborDouble -> NEXT_DOUBLE - } - + + internal var current: ElementHolder = element.tags.toMutableList() to element + private var listIterator: Iterator? = null + // Implementation of methods needed for CborTreeReader override fun nextTag(): ULong { - // In the structured parser, we don't actually read tags from a stream - // Instead, we return the first tag from the current element's tags - val tags = currentElement.tags - if (tags.isEmpty()) { + if (current.tags.isEmpty()) { throw CborDecodingException("Expected tag, but no tags found on current element") } - return tags[0] + return current.tags.removeFirst() } - override fun readByte(): Int { - // This is a no-op in the structured parser since we're not reading from a byte stream - // We just return the current byte representation - return curByte + override fun isNull() : Boolean { + //TODO this is a bit wonky! if we are inside a map, we want to skip over the key, and check the value, + // so the below call is not what it should be! + processTags(null) + return current.element is CborNull } - override fun isNull() = currentElement is CborNull - override fun isEnd() = when { - mapIterator != null -> !mapIterator!!.hasNext() listIterator != null -> !listIterator!!.hasNext() else -> false } override fun end() { // Reset iterators when ending a structure - mapIterator = null listIterator = null - currentMapEntry = null - currentListElement = null } - + override fun startArray(tags: ULongArray?): Int { processTags(tags) - if (currentElement !is CborList) { - throw CborDecodingException("Expected array, got ${currentElement::class.simpleName}") + if (current.element !is CborList) { + throw CborDecodingException("Expected array, got ${current.element::class.simpleName}") } - val list = currentElement as CborList + val list = current.element as CborList listIterator = list.iterator() return list.size } override fun startMap(tags: ULongArray?): Int { processTags(tags) - if (currentElement !is CborMap) { - throw CborDecodingException("Expected map, got ${currentElement::class.simpleName}") + if (current.element !is CborMap) { + throw CborDecodingException("Expected map, got ${current.element::class.simpleName}") } - val map = currentElement as CborMap - mapIterator = map.entries.iterator() - return map.size + val map = current.element as CborMap + //zip key, value, key, value, ... pairs to mirror byte-layout of CBOR map + listIterator = map.entries.flatMap { listOf(it.key, it.value) }.iterator() + return map.size //cbor map size is the size of the map, not the doubled size of the flattened pairs } override fun nextNull(tags: ULongArray?): Nothing? { processTags(tags) - if (currentElement !is CborNull) { - throw CborDecodingException("Expected null, got ${currentElement::class.simpleName}") + if (current.element !is CborNull) { + throw CborDecodingException("Expected null, got ${current.element::class.simpleName}") } return null } override fun nextBoolean(tags: ULongArray?): Boolean { processTags(tags) - if (currentElement !is CborBoolean) { - throw CborDecodingException("Expected boolean, got ${currentElement::class.simpleName}") + if (current.element !is CborBoolean) { + throw CborDecodingException("Expected boolean, got ${current.element::class.simpleName}") } - return (currentElement as CborBoolean).value + return (current.element as CborBoolean).value } override fun nextNumber(tags: ULongArray?): Long { processTags(tags) - return when (currentElement) { - is CborPositiveInt -> (currentElement as CborPositiveInt).value.toLong() - is CborNegativeInt -> (currentElement as CborNegativeInt).value - else -> throw CborDecodingException("Expected number, got ${currentElement::class.simpleName}") + return when (current.element) { + is CborPositiveInt -> (current.element as CborPositiveInt).value.toLong() + is CborNegativeInt -> (current.element as CborNegativeInt).value + else -> throw CborDecodingException("Expected number, got ${current.element::class.simpleName}") } } @@ -657,34 +641,32 @@ internal class StructuredCborParser(val element: CborElement, private val verify // Special handling for polymorphic serialization // If we have a CborList with a string as first element, return that string - if (currentElement is CborList && (currentElement as CborList).isNotEmpty() && (currentElement as CborList)[0] is CborString) { - val stringElement = (currentElement as CborList)[0] as CborString + if (current.element is CborList && (current.element as CborList).isNotEmpty() && (current.element as CborList)[0] is CborString) { + val stringElement = (current.element as CborList)[0] as CborString // Move to the next element (the map) for subsequent operations - currentElement = (currentElement as CborList)[1] + current = (current.element as CborList)[1].tags.toMutableList() to (current.element as CborList)[1] return stringElement.value } - if (currentElement !is CborString) { - throw CborDecodingException("Expected string, got ${currentElement::class.simpleName}") + if (current.element !is CborString) { + throw CborDecodingException("Expected string, got ${current.element::class.simpleName}") } - return (currentElement as CborString).value + return (current.element as CborString).value } override fun nextByteString(tags: ULongArray?): ByteArray { processTags(tags) - if (currentElement !is CborByteString) { - throw CborDecodingException("Expected byte string, got ${currentElement::class.simpleName}") + if (current.element !is CborByteString) { + throw CborDecodingException("Expected byte string, got ${current.element::class.simpleName}") } - return (currentElement as CborByteString).value + return (current.element as CborByteString).value } override fun nextDouble(tags: ULongArray?): Double { processTags(tags) - return when (currentElement) { - is CborDouble -> (currentElement as CborDouble).value - is CborPositiveInt -> (currentElement as CborPositiveInt).value.toDouble() - is CborNegativeInt -> (currentElement as CborNegativeInt).value.toDouble() - else -> throw CborDecodingException("Expected double, got ${currentElement::class.simpleName}") + return when (current.element) { + is CborDouble -> (current.element as CborDouble).value + else -> throw CborDecodingException("Expected double, got ${current.element::class.simpleName}") } } @@ -695,7 +677,7 @@ internal class StructuredCborParser(val element: CborElement, private val verify override fun nextTaggedStringOrNumber(): Triple { val tags = processTags(null) - return when (val key = currentMapEntry?.key) { + return when (val key = current.element) { is CborString -> Triple(key.value, null, tags) is CborPositiveInt -> Triple(null, key.value.toLong(), tags) is CborNegativeInt -> Triple(null, key.value, tags) @@ -704,29 +686,14 @@ internal class StructuredCborParser(val element: CborElement, private val verify } private fun processTags(tags: ULongArray?): ULongArray? { - val elementTags = currentElement.tags - + // If we're in a list, advance to the next element - if (listIterator != null && currentListElement == null && listIterator!!.hasNext()) { - currentListElement = listIterator!!.next() - currentElement = currentListElement!! - } - - // If we're in a map, advance to the next entry if we're not processing a value - if (mapIterator != null && currentMapEntry == null && mapIterator!!.hasNext()) { - currentMapEntry = mapIterator!!.next() - // We're now positioned at the key - currentElement = currentMapEntry!!.key - } - - // After processing a key in a map, move to the value for the next operation - if (mapIterator != null && currentMapEntry != null && currentElement == currentMapEntry!!.key) { - // We've processed the key, now move to the value - currentElement = currentMapEntry!!.value + if (listIterator != null && listIterator!!.hasNext()) { + listIterator!!.next().let { current = it.tags.toMutableList() to it } } // Store collected tags for verification - collectedTags = if (elementTags.isEmpty()) null else elementTags + val collectedTags = if (current.tags.isEmpty()) null else current.tags.toULongArray() // Verify tags if needed if (verifyObjectTags) { @@ -749,12 +716,6 @@ internal class StructuredCborParser(val element: CborElement, private val verify override fun skipElement(tags: ULongArray?) { // Process tags but don't do anything with the element processTags(tags) - - // If we're in a map and have processed a key, move to the value - if (mapIterator != null && currentMapEntry != null) { - currentElement = currentMapEntry!!.value - currentMapEntry = null - } } } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt index 9d7d082178..d2128b8c36 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt @@ -24,7 +24,6 @@ class CborDecoderTest { } @Test - @Ignore fun testDecodeComplicatedObject() { val test = TypesUmbrella( "Hello, world!", @@ -356,6 +355,7 @@ class CborDecoderTest { hex ) ) + val ref = ignoreUnknownKeys.encodeToCbor(expected) val struct = Cbor.decodeFromHexString(hex) assertEquals(expected, ignoreUnknownKeys.decodeFromCbor(SealedBox.serializer(), struct)) From a8c67975d4a8c02bbf6e1efa5ec977a70813b76a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Wed, 6 Aug 2025 19:57:41 +0200 Subject: [PATCH 14/68] clean up more --- .../cbor/internal/CborParserInterface.kt | 3 - .../serialization/cbor/internal/Decoder.kt | 170 +++++++++--------- .../serialization/cbor/CborDecoderTest.kt | 1 + 3 files changed, 82 insertions(+), 92 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt index 14296e519a..c2782f0c0f 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt @@ -37,7 +37,4 @@ internal sealed interface CborParserInterface { // Tag verification fun verifyTagsAndThrow(expected: ULongArray, actual: ULongArray?) - - // Additional methods needed for CborTreeReader - fun nextTag(): ULong } \ No newline at end of file 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 ef4e818901..ecfea2f802 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -16,9 +16,9 @@ internal open class CborReader(override val cbor: Cbor, protected val parser: Cb CborDecoder { override fun decodeCborElement(): CborElement = - when(parser) { - is CborParser -> CborTreeReader(cbor.configuration, parser).read() - is StructuredCborParser -> parser.element + when (parser) { + is CborParser -> CborTreeReader(cbor.configuration, parser).read() + is StructuredCborParser -> parser.element } @@ -58,7 +58,7 @@ internal open class CborReader(override val cbor: Cbor, protected val parser: Cb } override fun endStructure(descriptor: SerialDescriptor) { - if (!finiteMode) parser.end() + if (!finiteMode || parser is StructuredCborParser) parser.end() } override fun decodeElementIndex(descriptor: SerialDescriptor): Int { @@ -158,7 +158,8 @@ internal open class CborReader(override val cbor: Cbor, protected val parser: Cb } } -internal class CborParser(private val input: ByteArrayInput, private val verifyObjectTags: Boolean) : CborParserInterface { +internal class CborParser(private val input: ByteArrayInput, private val verifyObjectTags: Boolean) : + CborParserInterface { var curByte: Int = -1 init { @@ -196,7 +197,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO } } - override fun nextTag(): ULong { + fun nextTag(): ULong { if ((curByte shr 5) != 6) { throw CborDecodingException("Expected tag (major type 6), got major type ${curByte shr 5}") } @@ -554,157 +555,148 @@ private fun Iterable.flatten(): ByteArray { return output } -private typealias ElementHolder = Pair, CborElement> -private val ElementHolder.tags: MutableList get() = first -private val ElementHolder.element: CborElement get() = second -internal class StructuredCborParser(val element: CborElement, private val verifyObjectTags: Boolean) : CborParserInterface { - - internal var current: ElementHolder = element.tags.toMutableList() to element - private var listIterator: Iterator? = null +internal class StructuredCborParser(internal val element: CborElement, private val verifyObjectTags: Boolean) : + CborParserInterface { - // Implementation of methods needed for CborTreeReader - override fun nextTag(): ULong { - if (current.tags.isEmpty()) { - throw CborDecodingException("Expected tag, but no tags found on current element") - } - return current.tags.removeFirst() - } - - override fun isNull() : Boolean { - //TODO this is a bit wonky! if we are inside a map, we want to skip over the key, and check the value, - // so the below call is not what it should be! - processTags(null) - return current.element is CborNull + + internal var currentElement = element + private var listIterator: ListIterator? = null + private var isMap = false + private val isMapStack = ArrayDeque() + private val layerStack = ArrayDeque?>() + + override fun isNull(): Boolean { + return if (isMap) { + val isNull = listIterator!!.next() is CborNull + listIterator!!.previous() + isNull + } else currentElement is CborNull } - + override fun isEnd() = when { listIterator != null -> !listIterator!!.hasNext() else -> false } - + override fun end() { // Reset iterators when ending a structure - listIterator = null + isMap = isMapStack.removeLast() + listIterator = layerStack.removeLast() } override fun startArray(tags: ULongArray?): Int { processTags(tags) - if (current.element !is CborList) { - throw CborDecodingException("Expected array, got ${current.element::class.simpleName}") - } - - val list = current.element as CborList - listIterator = list.iterator() + if (currentElement !is CborList) { + throw CborDecodingException("Expected array, got ${currentElement::class.simpleName}") + } + isMapStack+=isMap + layerStack+=listIterator + isMap = false + val list = currentElement as CborList + listIterator = list.listIterator() return list.size } - + override fun startMap(tags: ULongArray?): Int { processTags(tags) - if (current.element !is CborMap) { - throw CborDecodingException("Expected map, got ${current.element::class.simpleName}") + if (currentElement !is CborMap) { + throw CborDecodingException("Expected map, got ${currentElement::class.simpleName}") } - - val map = current.element as CborMap + layerStack+=listIterator + isMapStack+=isMap + isMap = true + + val map = currentElement as CborMap //zip key, value, key, value, ... pairs to mirror byte-layout of CBOR map - listIterator = map.entries.flatMap { listOf(it.key, it.value) }.iterator() + listIterator = map.entries.flatMap { listOf(it.key, it.value) }.listIterator() return map.size //cbor map size is the size of the map, not the doubled size of the flattened pairs } - + override fun nextNull(tags: ULongArray?): Nothing? { processTags(tags) - if (current.element !is CborNull) { - throw CborDecodingException("Expected null, got ${current.element::class.simpleName}") + if (currentElement !is CborNull) { + throw CborDecodingException("Expected null, got ${currentElement::class.simpleName}") } return null } - + override fun nextBoolean(tags: ULongArray?): Boolean { processTags(tags) - if (current.element !is CborBoolean) { - throw CborDecodingException("Expected boolean, got ${current.element::class.simpleName}") + if (currentElement !is CborBoolean) { + throw CborDecodingException("Expected boolean, got ${currentElement::class.simpleName}") } - return (current.element as CborBoolean).value + return (currentElement as CborBoolean).value } - + override fun nextNumber(tags: ULongArray?): Long { processTags(tags) - return when (current.element) { - is CborPositiveInt -> (current.element as CborPositiveInt).value.toLong() - is CborNegativeInt -> (current.element as CborNegativeInt).value - else -> throw CborDecodingException("Expected number, got ${current.element::class.simpleName}") + return when (currentElement) { + is CborPositiveInt -> (currentElement as CborPositiveInt).value.toLong() + is CborNegativeInt -> (currentElement as CborNegativeInt).value + else -> throw CborDecodingException("Expected number, got ${currentElement::class.simpleName}") } } - + override fun nextString(tags: ULongArray?): String { processTags(tags) - - // Special handling for polymorphic serialization - // If we have a CborList with a string as first element, return that string - if (current.element is CborList && (current.element as CborList).isNotEmpty() && (current.element as CborList)[0] is CborString) { - val stringElement = (current.element as CborList)[0] as CborString - // Move to the next element (the map) for subsequent operations - current = (current.element as CborList)[1].tags.toMutableList() to (current.element as CborList)[1] - return stringElement.value - } - - if (current.element !is CborString) { - throw CborDecodingException("Expected string, got ${current.element::class.simpleName}") - } - return (current.element as CborString).value - } - + if (currentElement !is CborString) { + throw CborDecodingException("Expected string, got ${currentElement::class.simpleName}") + } + return (currentElement as CborString).value + } + override fun nextByteString(tags: ULongArray?): ByteArray { processTags(tags) - if (current.element !is CborByteString) { - throw CborDecodingException("Expected byte string, got ${current.element::class.simpleName}") + if (currentElement !is CborByteString) { + throw CborDecodingException("Expected byte string, got ${currentElement::class.simpleName}") } - return (current.element as CborByteString).value + return (currentElement as CborByteString).value } - + override fun nextDouble(tags: ULongArray?): Double { processTags(tags) - return when (current.element) { - is CborDouble -> (current.element as CborDouble).value - else -> throw CborDecodingException("Expected double, got ${current.element::class.simpleName}") + return when (currentElement) { + is CborDouble -> (currentElement as CborDouble).value + else -> throw CborDecodingException("Expected double, got ${currentElement::class.simpleName}") } } - + override fun nextFloat(tags: ULongArray?): Float { return nextDouble(tags).toFloat() } - + override fun nextTaggedStringOrNumber(): Triple { val tags = processTags(null) - - return when (val key = current.element) { + + return when (val key = currentElement) { is CborString -> Triple(key.value, null, tags) is CborPositiveInt -> Triple(null, key.value.toLong(), tags) is CborNegativeInt -> Triple(null, key.value, tags) else -> throw CborDecodingException("Expected string or number key, got ${key?.let { it::class.simpleName } ?: "null"}") } } - + private fun processTags(tags: ULongArray?): ULongArray? { // If we're in a list, advance to the next element if (listIterator != null && listIterator!!.hasNext()) { - listIterator!!.next().let { current = it.tags.toMutableList() to it } + currentElement= listIterator!!.next() } - + // Store collected tags for verification - val collectedTags = if (current.tags.isEmpty()) null else current.tags.toULongArray() - + val collectedTags = if (currentElement.tags.isEmpty()) null else currentElement.tags + // Verify tags if needed if (verifyObjectTags) { tags?.let { verifyTagsAndThrow(it, collectedTags) } } - + return collectedTags } - + override fun verifyTagsAndThrow(expected: ULongArray, actual: ULongArray?) { if (!expected.contentEquals(actual)) { throw CborDecodingException( @@ -712,7 +704,7 @@ internal class StructuredCborParser(val element: CborElement, private val verify ) } } - + override fun skipElement(tags: ULongArray?) { // Process tags but don't do anything with the element processTags(tags) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt index d2128b8c36..fa7d3b518d 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt @@ -47,6 +47,7 @@ class CborDecoderTest { ) val struct = Cbor.decodeFromHexString(hex) + assertEquals(Cbor.encodeToCbor(test), struct) assertEquals(test, Cbor.decodeFromCbor(TypesUmbrella.serializer(), struct)) From 745987efb5bbe10b8a62078448a5ad9760af9f76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Thu, 7 Aug 2025 08:02:00 +0200 Subject: [PATCH 15/68] fix structural issues --- .../cbor/internal/CborParserInterface.kt | 1 + .../serialization/cbor/internal/Decoder.kt | 16 +++++++++------- .../serialization/cbor/CborDecoderTest.kt | 1 - 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt index c2782f0c0f..8bb3401425 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt @@ -33,6 +33,7 @@ internal sealed interface CborParserInterface { fun nextTaggedStringOrNumber(): Triple // Skip operations + //used only to skip unknown elements fun skipElement(tags: ULongArray?) // Tag verification 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 ecfea2f802..84b660292a 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -585,17 +585,18 @@ internal class StructuredCborParser(internal val element: CborElement, private v listIterator = layerStack.removeLast() } + override fun startArray(tags: ULongArray?): Int { processTags(tags) if (currentElement !is CborList) { throw CborDecodingException("Expected array, got ${currentElement::class.simpleName}") } - isMapStack+=isMap - layerStack+=listIterator + isMapStack += isMap + layerStack += listIterator isMap = false val list = currentElement as CborList listIterator = list.listIterator() - return list.size + return -1 //just let the iterator run out of elements } override fun startMap(tags: ULongArray?): Int { @@ -603,14 +604,14 @@ internal class StructuredCborParser(internal val element: CborElement, private v if (currentElement !is CborMap) { throw CborDecodingException("Expected map, got ${currentElement::class.simpleName}") } - layerStack+=listIterator - isMapStack+=isMap + layerStack += listIterator + isMapStack += isMap isMap = true val map = currentElement as CborMap //zip key, value, key, value, ... pairs to mirror byte-layout of CBOR map listIterator = map.entries.flatMap { listOf(it.key, it.value) }.listIterator() - return map.size //cbor map size is the size of the map, not the doubled size of the flattened pairs + return -1// just let the iterator run out of elements } override fun nextNull(tags: ULongArray?): Nothing? { @@ -681,7 +682,7 @@ internal class StructuredCborParser(internal val element: CborElement, private v // If we're in a list, advance to the next element if (listIterator != null && listIterator!!.hasNext()) { - currentElement= listIterator!!.next() + currentElement = listIterator!!.next() } // Store collected tags for verification @@ -707,6 +708,7 @@ internal class StructuredCborParser(internal val element: CborElement, private v override fun skipElement(tags: ULongArray?) { // Process tags but don't do anything with the element + //TODO check for maps processTags(tags) } } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt index fa7d3b518d..e9b2346c5f 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt @@ -356,7 +356,6 @@ class CborDecoderTest { hex ) ) - val ref = ignoreUnknownKeys.encodeToCbor(expected) val struct = Cbor.decodeFromHexString(hex) assertEquals(expected, ignoreUnknownKeys.decodeFromCbor(SealedBox.serializer(), struct)) From c090ce0c6496d5f1adc2a5c5b0c026b31afa3552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Thu, 7 Aug 2025 10:13:36 +0200 Subject: [PATCH 16/68] fix map size regression --- .../src/kotlinx/serialization/cbor/internal/Decoder.kt | 5 ++--- .../src/kotlinx/serialization/cbor/SampleClasses.kt | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) 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 84b660292a..9c185a7e57 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -180,7 +180,6 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO override fun isNull() = (curByte == NULL || curByte == EMPTY_MAP || curByte == -1) - // Add this method to CborParser class private fun readUnsignedValueFromAdditionalInfo(additionalInfo: Int): Long { return when (additionalInfo) { in 0..23 -> additionalInfo.toLong() @@ -197,7 +196,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO } } - fun nextTag(): ULong { + internal fun nextTag(): ULong { if ((curByte shr 5) != 6) { throw CborDecodingException("Expected tag (major type 6), got major type ${curByte shr 5}") } @@ -717,7 +716,7 @@ internal class StructuredCborParser(internal val element: CborElement, private v private class CborMapReader(cbor: Cbor, decoder: CborParserInterface) : CborListReader(cbor, decoder) { override fun skipBeginToken(objectTags: ULongArray?) = setSize(parser.startMap(tags?.let { if (objectTags == null) it else ulongArrayOf(*it, *objectTags) } - ?: objectTags)) + ?: objectTags) * 2) } private open class CborListReader(cbor: Cbor, decoder: CborParserInterface) : CborReader(cbor, decoder) { diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/SampleClasses.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/SampleClasses.kt index 2db30b4a9f..311925dad4 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/SampleClasses.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/SampleClasses.kt @@ -73,8 +73,7 @@ data class NullableByteString( @ByteString val byteString: ByteArray? ) { override fun equals(other: Any?): Boolean { - return toString() == other.toString() - /* + if (this === other) return true if (other == null || this::class != other::class) return false @@ -85,7 +84,7 @@ data class NullableByteString( if (!byteString.contentEquals(other.byteString)) return false } else if (other.byteString != null) return false - return true*/ + return true } override fun hashCode(): Int { From 01371b0ab69b10cb00238b368c78e8e0cd733399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Thu, 7 Aug 2025 10:56:09 +0200 Subject: [PATCH 17/68] streamlining and cleanups --- .../kotlinx/serialization/cbor/CborDecoder.kt | 2 +- .../serialization/cbor/internal/Decoder.kt | 150 +++++++++++------- 2 files changed, 90 insertions(+), 62 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborDecoder.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborDecoder.kt index da6bde3c06..3a3de2279c 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborDecoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborDecoder.kt @@ -36,7 +36,7 @@ public interface CborDecoder : Decoder { * Decodes the next element in the current input as [CborElement]. * The type of the decoded element depends on the current state of the input and, when received * by [serializer][KSerializer] in its [KSerializer.serialize] method, the type of the token directly matches - * the [kind][SerialDescriptor.kind]. + * the [kind][kotlinx.serialization.descriptors.SerialDescriptor.kind]. * * This method is allowed to invoke only as the part of the whole deserialization process of the class, * calling this method after invoking [beginStructure] or any `decode*` method will lead to unspecified behaviour. 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 9c185a7e57..cd35e5ad21 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -554,111 +554,136 @@ private fun Iterable.flatten(): ByteArray { return output } +/** + * Iterator that keeps a reference to the current element and allows peeking at the next element. + * Works for single elements (where current is directly set to the element) and for collections (where current + * will be first set after `startMap` or `startArray` + */ +private class PeekingIterator(private val iter: ListIterator) : Iterator by iter { + + lateinit var current: T + private set + + override fun next(): T = iter.next().also { current = it } + + fun peek() = if (hasNext()) { + val next = iter.next() + iter.previous() + next + } else null + + companion object { + operator fun invoke(single: CborElement): PeekingIterator = + PeekingIterator(listOf(single).listIterator()).also { it.next() } + } +} +/** Maps are a bit special for nullability checks, so we need to check whether we're in a map or not*/ +private typealias CborLayer = Pair> + +private val CborLayer.isMap get() = first +/** current element reference inside a layer. If the layer is a primitive then the current element is the layer itself*/ +private val CborLayer.currentElement get() = second.current +private fun CborLayer.peek() = second.peek() +private fun CborLayer.hasNext() = second.hasNext() +private fun CborLayer.nextElement() = second.next() + +/** + * CBOR parser that operates on [CborElement] instead of bytes. Closely mirrors the baheviour of [CborParser], so the + * [CborDecoder] can remain largely unchanged. + */ internal class StructuredCborParser(internal val element: CborElement, private val verifyObjectTags: Boolean) : CborParserInterface { + private var layer: CborLayer = false to PeekingIterator(element) - internal var currentElement = element - private var listIterator: ListIterator? = null - private var isMap = false - private val isMapStack = ArrayDeque() - private val layerStack = ArrayDeque?>() - override fun isNull(): Boolean { - return if (isMap) { - val isNull = listIterator!!.next() is CborNull - listIterator!!.previous() - isNull - } else currentElement is CborNull - } + private val layerStack = ArrayDeque() - override fun isEnd() = when { - listIterator != null -> !listIterator!!.hasNext() - else -> false - } + // map needs special treatment because keys and values are laid out as a list alternating between key and value to + // mirror the byte-layout of a cbor map. + override fun isNull() = if (layer.isMap) layer.peek() is CborNull else layer.currentElement is CborNull + + override fun isEnd() = !layer.hasNext() override fun end() { // Reset iterators when ending a structure - isMap = isMapStack.removeLast() - listIterator = layerStack.removeLast() + layer = layerStack.removeLast() } - override fun startArray(tags: ULongArray?): Int { processTags(tags) - if (currentElement !is CborList) { - throw CborDecodingException("Expected array, got ${currentElement::class.simpleName}") + if (layer.currentElement !is CborList) { + throw CborDecodingException("Expected array, got ${layer.currentElement::class.simpleName}") } - isMapStack += isMap - layerStack += listIterator - isMap = false - val list = currentElement as CborList - listIterator = list.listIterator() - return -1 //just let the iterator run out of elements + layerStack += layer + val list = layer.currentElement as CborList + layer = false to PeekingIterator(list.listIterator()) + return list.size //we could just return -1 and let the current layer run out of elements to never run into inconsistencies + // if we do keep it like this, any inconsistencies serve as a canary for implementation bugs } override fun startMap(tags: ULongArray?): Int { processTags(tags) - if (currentElement !is CborMap) { - throw CborDecodingException("Expected map, got ${currentElement::class.simpleName}") + if (layer.currentElement !is CborMap) { + throw CborDecodingException("Expected map, got ${layer.currentElement::class.simpleName}") } - layerStack += listIterator - isMapStack += isMap - isMap = true + layerStack += layer - val map = currentElement as CborMap - //zip key, value, key, value, ... pairs to mirror byte-layout of CBOR map - listIterator = map.entries.flatMap { listOf(it.key, it.value) }.listIterator() - return -1// just let the iterator run out of elements + val map = layer.currentElement as CborMap + // zip key, value, key, value, ... pairs to mirror byte-layout of CBOR map, so decoding this here works the same + // as decoding from bytes + layer = true to PeekingIterator(map.entries.flatMap { listOf(it.key, it.value) }.listIterator()) + return map.size//we could just return -1 and let the current layer run out of elements to never run into inconsistencies + // if we do keep it like this, any inconsistencies serve as a canary for implementation bugs } override fun nextNull(tags: ULongArray?): Nothing? { processTags(tags) - if (currentElement !is CborNull) { - throw CborDecodingException("Expected null, got ${currentElement::class.simpleName}") + if (layer.currentElement !is CborNull) { + throw CborDecodingException("Expected null, got ${layer.currentElement::class.simpleName}") } return null } override fun nextBoolean(tags: ULongArray?): Boolean { processTags(tags) - if (currentElement !is CborBoolean) { - throw CborDecodingException("Expected boolean, got ${currentElement::class.simpleName}") + if (layer.currentElement !is CborBoolean) { + throw CborDecodingException("Expected boolean, got ${layer.currentElement::class.simpleName}") } - return (currentElement as CborBoolean).value + return (layer.currentElement as CborBoolean).value } override fun nextNumber(tags: ULongArray?): Long { processTags(tags) - return when (currentElement) { - is CborPositiveInt -> (currentElement as CborPositiveInt).value.toLong() - is CborNegativeInt -> (currentElement as CborNegativeInt).value - else -> throw CborDecodingException("Expected number, got ${currentElement::class.simpleName}") + return when (layer.currentElement) { + is CborPositiveInt -> (layer.currentElement as CborPositiveInt).value.toLong() + is CborNegativeInt -> (layer.currentElement as CborNegativeInt).value + else -> throw CborDecodingException("Expected number, got ${layer.currentElement::class.simpleName}") } } override fun nextString(tags: ULongArray?): String { processTags(tags) - if (currentElement !is CborString) { - throw CborDecodingException("Expected string, got ${currentElement::class.simpleName}") + if (layer.currentElement !is CborString) { + throw CborDecodingException("Expected string, got ${layer.currentElement::class.simpleName}") } - return (currentElement as CborString).value + return (layer.currentElement as CborString).value } override fun nextByteString(tags: ULongArray?): ByteArray { processTags(tags) - if (currentElement !is CborByteString) { - throw CborDecodingException("Expected byte string, got ${currentElement::class.simpleName}") + if (layer.currentElement !is CborByteString) { + throw CborDecodingException("Expected byte string, got ${layer.currentElement::class.simpleName}") } - return (currentElement as CborByteString).value + return (layer.currentElement as CborByteString).value } override fun nextDouble(tags: ULongArray?): Double { processTags(tags) - return when (currentElement) { - is CborDouble -> (currentElement as CborDouble).value - else -> throw CborDecodingException("Expected double, got ${currentElement::class.simpleName}") + return when (layer.currentElement) { + is CborDouble -> (layer.currentElement as CborDouble).value + else -> throw CborDecodingException("Expected double, got ${layer.currentElement::class.simpleName}") } } @@ -669,7 +694,7 @@ internal class StructuredCborParser(internal val element: CborElement, private v override fun nextTaggedStringOrNumber(): Triple { val tags = processTags(null) - return when (val key = currentElement) { + return when (val key = layer.currentElement) { is CborString -> Triple(key.value, null, tags) is CborPositiveInt -> Triple(null, key.value.toLong(), tags) is CborNegativeInt -> Triple(null, key.value, tags) @@ -677,15 +702,19 @@ internal class StructuredCborParser(internal val element: CborElement, private v } } + /** + * Verify the current element's object tags and advance to the next element if inside a list/map. + * The reason this method mixes two behaviours is that decoding a primitive is invoked on a single element. + * `decodeElementIndex`, etc. is invoked on an iterable and there are key tags and value tags + */ private fun processTags(tags: ULongArray?): ULongArray? { - // If we're in a list, advance to the next element - if (listIterator != null && listIterator!!.hasNext()) { - currentElement = listIterator!!.next() - } + // If we're in a list/map, advance to the next element + if (layer.hasNext()) layer.nextElement() + // if we're at a primitive, we only process tags // Store collected tags for verification - val collectedTags = if (currentElement.tags.isEmpty()) null else currentElement.tags + val collectedTags = if (layer.currentElement.tags.isEmpty()) null else layer.currentElement.tags // Verify tags if needed if (verifyObjectTags) { @@ -707,7 +736,6 @@ internal class StructuredCborParser(internal val element: CborElement, private v override fun skipElement(tags: ULongArray?) { // Process tags but don't do anything with the element - //TODO check for maps processTags(tags) } } From 4930de8037ee8b36176f58ae0dcf1bbd885e3985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Thu, 7 Aug 2025 13:11:58 +0200 Subject: [PATCH 18/68] benchmarks --- .../kotlinx/benchmarks/cbor/CborBaseLine.kt | 14 +++++ .../serialization/cbor/internal/Encoder.kt | 56 ++++++++++--------- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/benchmark/src/jmh/kotlin/kotlinx/benchmarks/cbor/CborBaseLine.kt b/benchmark/src/jmh/kotlin/kotlinx/benchmarks/cbor/CborBaseLine.kt index 8b71d92b7f..fe7222e8e4 100644 --- a/benchmark/src/jmh/kotlin/kotlinx/benchmarks/cbor/CborBaseLine.kt +++ b/benchmark/src/jmh/kotlin/kotlinx/benchmarks/cbor/CborBaseLine.kt @@ -52,6 +52,7 @@ open class CborBaseline { } val baseBytes = cbor.encodeToByteArray(KTestOuterMessage.serializer(), baseMessage) + val baseStruct = cbor.encodeToCbor(KTestOuterMessage.serializer(), baseMessage) @Benchmark fun toBytes() = cbor.encodeToByteArray(KTestOuterMessage.serializer(), baseMessage) @@ -59,4 +60,17 @@ open class CborBaseline { @Benchmark fun fromBytes() = cbor.decodeFromByteArray(KTestOuterMessage.serializer(), baseBytes) + + @Benchmark + fun structToBytes() = cbor.encodeToByteArray(CborElement.serializer(), baseStruct) + + @Benchmark + fun structFromBytes() = cbor.decodeFromByteArray(CborElement.serializer(), baseBytes) + + @Benchmark + fun fromStruct() = cbor.decodeFromCbor(KTestOuterMessage.serializer(), baseStruct) + + @Benchmark + fun toStruct() = cbor.encodeToCbor(KTestOuterMessage.serializer(), baseMessage) + } 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 138fedca5e..c047142817 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -186,13 +186,14 @@ internal class IndefiniteLengthCborWriter(cbor: Cbor, private val output: ByteAr } // optimized indefinite length encoder -internal class StructuredCborWriter(cbor: Cbor) : CborWriter( - cbor -) { +internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { - sealed class CborContainer(tags: ULongArray, elements: MutableList) { - var elements = elements - private set + /** + * Tags and values are "written", i.e. recorded/prepared for encoding separately. Hence, we need a helper that allows + * for setting tags and values independently, and then merging them into the final [CborElement] at the end. + */ + internal sealed class CborContainer(tags: ULongArray, elements: MutableList) { + protected val elements = elements var tags = tags internal set @@ -207,7 +208,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter( class Primitive(tags: ULongArray) : CborContainer(tags, elements = mutableListOf()) { override fun add(element: CborElement): Boolean { - require(elements.isEmpty()) {"Implementation error. Please report a bug."} + require(elements.isEmpty()) { "Implementation error. Please report a bug." } return elements.add(element) } } @@ -226,6 +227,11 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter( } } + private operator fun CborContainer?.plusAssign(element: CborElement) { + this!!.add(element) + } + + private val stack = ArrayDeque() private var currentElement: CborContainer? = null @@ -252,14 +258,14 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter( val finalized = currentElement!!.finalize() if (stack.isNotEmpty()) { currentElement = stack.removeLast() - currentElement!!.add(finalized) + currentElement += finalized } } override fun getDestination() = throw IllegalStateException("There is not byteArrayOutput") - - override fun incrementChildren() {/*NOOP*/ + override fun incrementChildren() { + /*NOOP*/ } @@ -276,11 +282,9 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter( //indices are put into the name field. we don't want to write those, as it would result in double writes val cborLabel = descriptor.getCborLabel(index) if (cbor.configuration.preferCborLabelsOverNames && cborLabel != null) { - currentElement!!.add( - CborInt.invoke(value = cborLabel, tags = keyTags ?: ulongArrayOf()) - ) + currentElement += CborInt(value = cborLabel, tags = keyTags ?: ulongArrayOf()) } else { - currentElement!!.add(CborString(name, keyTags ?: ulongArrayOf())) + currentElement += CborString(name, keyTags ?: ulongArrayOf()) } } } @@ -295,51 +299,51 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter( override fun encodeBoolean(value: Boolean) { - currentElement!!.add(CborBoolean(value)) + currentElement += CborBoolean(value) } override fun encodeByte(value: Byte) { - currentElement!!.add(CborInt(value.toLong())) + currentElement += CborInt(value.toLong()) } override fun encodeChar(value: Char) { - currentElement!!.add(CborInt(value.code.toLong())) + currentElement += CborInt(value.code.toLong()) } override fun encodeDouble(value: Double) { - currentElement!!.add(CborDouble(value)) + currentElement += CborDouble(value) } override fun encodeFloat(value: Float) { - currentElement!!.add(CborDouble(value.toDouble())) + currentElement += CborDouble(value.toDouble()) } override fun encodeInt(value: Int) { - currentElement!!.add(CborInt(value.toLong())) + currentElement += CborInt(value.toLong()) } override fun encodeLong(value: Long) { - currentElement!!.add(CborInt(value)) + currentElement += CborInt(value) } override fun encodeShort(value: Short) { - currentElement!!.add(CborInt(value.toLong())) + currentElement += CborInt(value.toLong()) } override fun encodeString(value: String) { - currentElement!!.add(CborString(value)) + currentElement += CborString(value) } override fun encodeByteString(byteArray: ByteArray) { - currentElement!!.add(CborByteString(byteArray)) + currentElement += CborByteString(byteArray) } override fun encodeNull() { - currentElement!!.add(CborNull()) + currentElement += CborNull() } override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) { - currentElement!!.add(CborString(enumDescriptor.getElementName(index))) + currentElement += CborString(enumDescriptor.getElementName(index)) } } From 0139a288109761cc21f4f9aa3128e7272b0487ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Thu, 7 Aug 2025 14:00:04 +0200 Subject: [PATCH 19/68] add more tests --- .../serialization/cbor/CborArrayTest.kt | 27 ++++++++++++++----- .../serialization/cbor/CborDecoderTest.kt | 19 ++++++++++--- .../kotlinx/serialization/cbor/CborIsoTest.kt | 4 +++ 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborArrayTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborArrayTest.kt index c0b859566b..619c951e7e 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborArrayTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborArrayTest.kt @@ -1,6 +1,7 @@ package kotlinx.serialization.cbor import kotlinx.serialization.* +import kotlinx.serialization.cbor.CborIsoTest.DataClass import kotlin.test.* @@ -18,6 +19,10 @@ class CborArrayTest { val cbor = Cbor.CoseCompliant assertEquals(referenceHexString, cbor.encodeToHexString(ClassAs1Array.serializer(), reference)) assertEquals(reference, cbor.decodeFromHexString(ClassAs1Array.serializer(), referenceHexString)) + + val struct = cbor.encodeToCbor(ClassAs1Array.serializer(), reference) + assertEquals(reference, cbor.decodeFromCbor(ClassAs1Array.serializer(), struct)) + assertEquals(referenceHexString, cbor.encodeToHexString(CborElement.serializer(), struct)) } @Test @@ -35,6 +40,10 @@ class CborArrayTest { val cbor = Cbor.CoseCompliant assertEquals(referenceHexString, cbor.encodeToHexString(ClassAs2Array.serializer(), reference)) assertEquals(reference, cbor.decodeFromHexString(ClassAs2Array.serializer(), referenceHexString)) + + val struct = cbor.encodeToCbor(ClassAs2Array.serializer(), reference) + assertEquals(reference, cbor.decodeFromCbor(ClassAs2Array.serializer(), struct)) + assertEquals(referenceHexString, cbor.encodeToHexString(CborElement.serializer(), struct)) } @Test @@ -54,6 +63,10 @@ class CborArrayTest { assertEquals(referenceHexString, cbor.encodeToHexString(ClassAs4ArrayNullable.serializer(), reference)) assertEquals(reference, cbor.decodeFromHexString(ClassAs4ArrayNullable.serializer(), referenceHexString)) + + val struct = cbor.encodeToCbor(ClassAs4ArrayNullable.serializer(), reference) + assertEquals(reference, cbor.decodeFromCbor(ClassAs4ArrayNullable.serializer(), struct)) + assertEquals(referenceHexString, cbor.encodeToHexString(CborElement.serializer(), struct)) } @Test @@ -75,12 +88,10 @@ class CborArrayTest { assertEquals(referenceHexString, cbor.encodeToHexString(ClassWithArray.serializer(), reference)) assertEquals(reference, cbor.decodeFromHexString(ClassWithArray.serializer(), referenceHexString)) - println( - cbor.encodeToHexString( - DoubleTaggedClassWithArray.serializer(), - DoubleTaggedClassWithArray(array = ClassAs2Array(alg = -7, kid = "bar")) - ) - ) + + val struct = cbor.encodeToCbor(ClassWithArray.serializer(), reference) + assertEquals(reference, cbor.decodeFromCbor(ClassWithArray.serializer(), struct)) + assertEquals(referenceHexString, cbor.encodeToHexString(CborElement.serializer(), struct)) } @@ -103,6 +114,10 @@ class CborArrayTest { val cbor = Cbor.CoseCompliant assertEquals(referenceHexString, cbor.encodeToHexString(DoubleTaggedClassWithArray.serializer(), reference)) assertEquals(reference, cbor.decodeFromHexString(DoubleTaggedClassWithArray.serializer(), referenceHexString)) + + val struct = cbor.encodeToCbor(DoubleTaggedClassWithArray.serializer(), reference) + assertEquals(reference, cbor.decodeFromCbor(DoubleTaggedClassWithArray.serializer(), struct)) + assertEquals(referenceHexString, cbor.encodeToHexString(CborElement.serializer(), struct)) } @CborArray diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt index e9b2346c5f..a95c3a7c17 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt @@ -18,9 +18,13 @@ class CborDecoderTest { @Test fun testDecodeSimpleObject() { val hex = "bf616163737472ff" - assertEquals(Simple("str"), Cbor.decodeFromHexString(Simple.serializer(), hex)) + val reference = Simple("str") + assertEquals(reference, Cbor.decodeFromHexString(Simple.serializer(), hex)) + val struct = Cbor.decodeFromHexString(hex) - assertEquals(Simple("str"), Cbor.decodeFromCbor(Simple.serializer(), struct)) + assertEquals(reference, Cbor.decodeFromCbor(Simple.serializer(), struct)) + + assertEquals(hex, Cbor.encodeToHexString(CborElement.serializer(), struct)) } @Test @@ -50,14 +54,23 @@ class CborDecoderTest { assertEquals(Cbor.encodeToCbor(test), struct) assertEquals(test, Cbor.decodeFromCbor(TypesUmbrella.serializer(), struct)) + assertEquals(hex, Cbor.encodeToHexString(CborElement.serializer(), struct)) + // with maps, lists & strings of definite length + val hexDef = + "a9646c6973748261616162686e756c6c61626c65f6636d6170a202f401f56169182a6a696e6e6572734c69737481a16161636b656b637374726d48656c6c6f2c20776f726c642165696e6e6572a16161636c6f6c6a62797465537472696e6742cafe6962797465417272617982383521" assertEquals( test, Cbor.decodeFromHexString( TypesUmbrella.serializer(), - "a9646c6973748261616162686e756c6c61626c65f6636d6170a202f401f56169182a6a696e6e6572734c69737481a16161636b656b637374726d48656c6c6f2c20776f726c642165696e6e6572a16161636c6f6c6a62797465537472696e6742cafe6962797465417272617982383521" + hexDef ) ) + + val structDef = Cbor.decodeFromHexString(hexDef) + assertEquals(Cbor.encodeToCbor(test), structDef) + assertEquals(test, Cbor.decodeFromCbor(TypesUmbrella.serializer(), structDef)) + } @Test diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborIsoTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborIsoTest.kt index 49ef3390ae..03651091f1 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborIsoTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborIsoTest.kt @@ -27,6 +27,10 @@ class CborIsoTest { } assertEquals(reference, cbor.decodeFromHexString(DataClass.serializer(), referenceHexString)) assertEquals(referenceHexString, cbor.encodeToHexString(DataClass.serializer(), reference)) + + val struct = cbor.encodeToCbor(DataClass.serializer(), reference) + assertEquals(reference, cbor.decodeFromCbor(DataClass.serializer(), struct)) + assertEquals(referenceHexString, cbor.encodeToHexString(CborElement.serializer(), struct)) } @Serializable From f64740dd407fa392329399d9d9e5d2c809ed0630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Thu, 7 Aug 2025 14:15:10 +0200 Subject: [PATCH 20/68] clarify faulty test vector --- .../src/kotlinx/serialization/cbor/CborDecoderTest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt index a95c3a7c17..a13f550488 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt @@ -54,9 +54,13 @@ class CborDecoderTest { assertEquals(Cbor.encodeToCbor(test), struct) assertEquals(test, Cbor.decodeFromCbor(TypesUmbrella.serializer(), struct)) + + assertEquals(hex, Cbor.encodeToHexString(TypesUmbrella.serializer(), test)) + assertEquals(hex, Cbor.encodeToHexString(CborElement.serializer(), struct)) + // with maps, lists & strings of definite length val hexDef = "a9646c6973748261616162686e756c6c61626c65f6636d6170a202f401f56169182a6a696e6e6572734c69737481a16161636b656b637374726d48656c6c6f2c20776f726c642165696e6e6572a16161636c6f6c6a62797465537472696e6742cafe6962797465417272617982383521" From fb55e233eabf14d7fac83db34c98d92b2605b0e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Thu, 7 Aug 2025 15:01:58 +0200 Subject: [PATCH 21/68] fix test vector --- .../src/kotlinx/serialization/cbor/CborDecoderTest.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt index a13f550488..d683acb2d4 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt @@ -40,9 +40,10 @@ class CborDecoderTest { HexConverter.parseHexBinary("cafe"), HexConverter.parseHexBinary("cafe") ) - // with maps, lists & strings of indefinite length + // with maps, lists & strings of indefinite length (note: this test vector did not correspond to proper encoding before, but decoded fine) + // this collapsing bytes wrapped in a bytes string into a byte string could be an indicator of a buggy (as in: too lenient) decoder. val hex = - "bf637374726d48656c6c6f2c20776f726c64216169182a686e756c6c61626c65f6646c6973749f61616162ff636d6170bf01f502f4ff65696e6e6572bf6161636c6f6cff6a696e6e6572734c6973749fbf6161636b656bffff6a62797465537472696e675f42cafeff696279746541727261799f383521ffff" + "bf637374726d48656c6c6f2c20776f726c64216169182a686e756c6c61626c65f6646c6973749f61616162ff636d6170bf01f502f4ff65696e6e6572bf6161636c6f6cff6a696e6e6572734c6973749fbf6161636b656bffff6a62797465537472696e6742cafe696279746541727261799f383521ffff" assertEquals( test, Cbor.decodeFromHexString( TypesUmbrella.serializer(), @@ -54,9 +55,7 @@ class CborDecoderTest { assertEquals(Cbor.encodeToCbor(test), struct) assertEquals(test, Cbor.decodeFromCbor(TypesUmbrella.serializer(), struct)) - assertEquals(hex, Cbor.encodeToHexString(TypesUmbrella.serializer(), test)) - assertEquals(hex, Cbor.encodeToHexString(CborElement.serializer(), struct)) From 6e03f681a3e3947edd033e0b6238b8ec75f27880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Thu, 7 Aug 2025 19:40:50 +0200 Subject: [PATCH 22/68] Fix Tagging and Simplify --- .../kotlinx/serialization/cbor/CborElement.kt | 32 +- .../cbor/internal/CborElementSerializers.kt | 93 +-- .../cbor/internal/CborParserInterface.kt | 3 + .../cbor/internal/CborTreeReader.kt | 28 +- .../serialization/cbor/internal/Decoder.kt | 115 ++-- .../serialization/cbor/internal/Encoder.kt | 73 ++- .../serialization/cbor/CborArrayTest.kt | 2 + .../serialization/cbor/CborDecoderTest.kt | 51 +- .../cbor/CborDefiniteLengthTest.kt | 9 + .../cbor/CborElementEqualityTest.kt | 126 ++-- .../serialization/cbor/CborElementTest.kt | 339 ++++++++++- .../serialization/cbor/CborLabelTest.kt | 39 ++ .../cbor/CborPolymorphismTest.kt | 19 +- .../serialization/cbor/CborTaggedTest.kt | 540 ++++++++++++------ 14 files changed, 1062 insertions(+), 407 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt index c6f610b18d..42c6f0bcd9 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -89,6 +89,7 @@ public sealed class CborPrimitive( } } +@Serializable(with = CborIntSerializer::class) public sealed class CborInt( tags: ULongArray = ulongArrayOf(), value: T, @@ -96,23 +97,24 @@ public sealed class CborInt( public companion object { public operator fun invoke( value: Long, - tags: ULongArray = ulongArrayOf() - ): CborInt<*> = if (value >= 0) CborPositiveInt(value.toULong(), tags) else CborNegativeInt(value, tags) + vararg tags: ULong + ): CborInt<*> = + if (value >= 0) CborPositiveInt(value.toULong(), tags = tags) else CborNegativeInt(value, tags = tags) public operator fun invoke( value: ULong, - tags: ULongArray = ulongArrayOf() - ): CborInt = CborPositiveInt(value, tags) + vararg tags: ULong + ): CborInt = CborPositiveInt(value, tags = tags) } } /** * Class representing signed CBOR integer (major type 1). */ -@Serializable(with = CborIntSerializer::class) +@Serializable(with = CborNegativeIntSerializer::class) public class CborNegativeInt( value: Long, - tags: ULongArray = ulongArrayOf() + vararg tags: ULong ) : CborInt(tags, value) { init { require(value < 0) { "Number must be negative: $value" } @@ -122,10 +124,10 @@ public class CborNegativeInt( /** * Class representing unsigned CBOR integer (major type 0). */ -@Serializable(with = CborUIntSerializer::class) +@Serializable(with = CborPositiveIntSerializer::class) public class CborPositiveInt( value: ULong, - tags: ULongArray = ulongArrayOf() + vararg tags: ULong ) : CborInt(tags, value) /** @@ -134,7 +136,7 @@ public class CborPositiveInt( @Serializable(with = CborDoubleSerializer::class) public class CborDouble( value: Double, - tags: ULongArray = ulongArrayOf() + vararg tags: ULong ) : CborPrimitive(value, tags) /** @@ -143,7 +145,7 @@ public class CborDouble( @Serializable(with = CborStringSerializer::class) public class CborString( value: String, - tags: ULongArray = ulongArrayOf() + vararg tags: ULong ) : CborPrimitive(value, tags) /** @@ -152,7 +154,7 @@ public class CborString( @Serializable(with = CborBooleanSerializer::class) public class CborBoolean( value: Boolean, - tags: ULongArray = ulongArrayOf() + vararg tags: ULong ) : CborPrimitive(value, tags) /** @@ -161,7 +163,7 @@ public class CborBoolean( @Serializable(with = CborByteStringSerializer::class) public class CborByteString( value: ByteArray, - tags: ULongArray = ulongArrayOf() + vararg tags: ULong ) : CborPrimitive(value, tags) { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -189,7 +191,7 @@ public class CborByteString( * Class representing CBOR `null` value */ @Serializable(with = CborNullSerializer::class) -public class CborNull(tags: ULongArray = ulongArrayOf()) : CborPrimitive(Unit, tags) +public class CborNull(vararg tags: ULong) : CborPrimitive(Unit, tags) /** * Class representing CBOR map, consisting of key-value pairs, where both key and value are arbitrary [CborElement] @@ -200,7 +202,7 @@ public class CborNull(tags: ULongArray = ulongArrayOf()) : CborPrimitive(U @Serializable(with = CborMapSerializer::class) public class CborMap( private val content: Map, - tags: ULongArray = ulongArrayOf() + vararg tags: ULong ) : CborElement(tags), Map by content { public override fun equals(other: Any?): Boolean = @@ -226,7 +228,7 @@ public class CborMap( @Serializable(with = CborListSerializer::class) public class CborList( private val content: List, - tags: ULongArray = ulongArrayOf() + vararg tags: ULong ) : CborElement(tags), List by content { public override fun equals(other: Any?): Boolean = diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index 3c968d0714..53d72655c7 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -11,11 +11,13 @@ import kotlinx.serialization.cbor.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* +internal interface CborSerializer + /** * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborElement]. * It can only be used by with [Cbor] format and its input ([CborDecoder] and [CborEncoder]). */ -internal object CborElementSerializer : KSerializer { +internal object CborElementSerializer : KSerializer, CborSerializer { override val descriptor: SerialDescriptor = buildSerialDescriptor("kotlinx.serialization.cbor.CborElement", PolymorphicKind.SEALED) { // Resolve cyclic dependency in descriptors by late binding @@ -27,8 +29,8 @@ internal object CborElementSerializer : KSerializer { element("CborMap", defer { CborMapSerializer.descriptor }) element("CborList", defer { CborListSerializer.descriptor }) element("CborDouble", defer { CborDoubleSerializer.descriptor }) - element("CborInt", defer { CborIntSerializer.descriptor }) - element("CborUInt", defer { CborUIntSerializer.descriptor }) + element("CborInt", defer { CborNegativeIntSerializer.descriptor }) + element("CborUInt", defer { CborPositiveIntSerializer.descriptor }) } override fun serialize(encoder: Encoder, value: CborElement) { @@ -52,23 +54,19 @@ internal object CborElementSerializer : KSerializer { * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborPrimitive]. * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). */ -internal object CborPrimitiveSerializer : KSerializer> { +internal object CborPrimitiveSerializer : KSerializer>, CborSerializer { override val descriptor: SerialDescriptor = - buildSerialDescriptor("kotlinx.serialization.cbor.CborPrimitive", PrimitiveKind.STRING) + buildSerialDescriptor("kotlinx.serialization.cbor.CborPrimitive", PolymorphicKind.SEALED) override fun serialize(encoder: Encoder, value: CborPrimitive<*>) { - val cborEncoder = encoder.asCborEncoder() - - cborEncoder.encodeTags(value) - when (value) { is CborNull -> encoder.encodeSerializableValue(CborNullSerializer, value) is CborString -> encoder.encodeSerializableValue(CborStringSerializer, value) is CborBoolean -> encoder.encodeSerializableValue(CborBooleanSerializer, value) is CborByteString -> encoder.encodeSerializableValue(CborByteStringSerializer, value) is CborDouble -> encoder.encodeSerializableValue(CborDoubleSerializer, value) - is CborNegativeInt -> encoder.encodeSerializableValue(CborIntSerializer, value) - is CborPositiveInt -> encoder.encodeSerializableValue(CborUIntSerializer, value) + is CborNegativeInt -> encoder.encodeSerializableValue(CborNegativeIntSerializer, value) + is CborPositiveInt -> encoder.encodeSerializableValue(CborPositiveIntSerializer, value) } } @@ -83,13 +81,14 @@ internal object CborPrimitiveSerializer : KSerializer> { * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborNull]. * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). */ -internal object CborNullSerializer : KSerializer { +internal object CborNullSerializer : KSerializer, CborSerializer { override val descriptor: SerialDescriptor = buildSerialDescriptor("kotlinx.serialization.cbor.CborNull", SerialKind.ENUM) override fun serialize(encoder: Encoder, value: CborNull) { - encoder.asCborEncoder().encodeTags(value) + val cborEncoder = encoder.asCborEncoder() + cborEncoder.encodeTags(value) encoder.encodeNull() } @@ -98,16 +97,38 @@ internal object CborNullSerializer : KSerializer { if (decoder.decodeNotNullMark()) { throw CborDecodingException("Expected 'null' literal") } + decoder.decodeNull() return CborNull() } } -internal object CborIntSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborInt", PrimitiveKind.LONG) + +internal object CborIntSerializer : KSerializer>, CborSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborInt", PrimitiveKind.LONG) + + override fun serialize(encoder: Encoder, value: CborInt<*>) { + when (value) { + is CborNegativeInt -> encoder.encodeSerializableValue(CborNegativeIntSerializer, value) + is CborPositiveInt -> encoder.encodeSerializableValue(CborPositiveIntSerializer, value) + } + } + + override fun deserialize(decoder: Decoder): CborInt<*> { + val result = decoder.asCborDecoder().decodeCborElement() + if (result !is CborInt<*>) throw CborDecodingException("Unexpected CBOR element, expected CborInt, had ${result::class}") + return result + } +} + +internal object CborNegativeIntSerializer : KSerializer, CborSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborNegativeInt", PrimitiveKind.LONG) override fun serialize(encoder: Encoder, value: CborNegativeInt) { - encoder.asCborEncoder().encodeTags(value) + val cborEncoder = encoder.asCborEncoder() + cborEncoder.encodeTags(value) encoder.encodeLong(value.value) } @@ -117,12 +138,14 @@ internal object CborIntSerializer : KSerializer { } } -internal object CborUIntSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("CborUInt", PrimitiveKind.LONG) +internal object CborPositiveIntSerializer : KSerializer, CborSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborPositiveInt", PrimitiveKind.LONG) override fun serialize(encoder: Encoder, value: CborPositiveInt) { - encoder.asCborEncoder().encodeTags(value) - encoder.encodeInline(descriptor).encodeSerializableValue(ULong.serializer(), value.value) + val cborEncoder = encoder.asCborEncoder() + cborEncoder.encodeTags(value) + encoder.encodeInline(descriptor).encodeSerializableValue(ULong.serializer(), value.value as ULong) } override fun deserialize(decoder: Decoder): CborPositiveInt { @@ -131,11 +154,13 @@ internal object CborUIntSerializer : KSerializer { } } -internal object CborDoubleSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborDouble", PrimitiveKind.DOUBLE) +internal object CborDoubleSerializer : KSerializer, CborSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborDouble", PrimitiveKind.DOUBLE) override fun serialize(encoder: Encoder, value: CborDouble) { - encoder.asCborEncoder().encodeTags(value) + val cborEncoder = encoder.asCborEncoder() + cborEncoder.encodeTags(value) encoder.encodeDouble(value.value) } @@ -149,12 +174,13 @@ internal object CborDoubleSerializer : KSerializer { * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborString]. * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). */ -internal object CborStringSerializer : KSerializer { +internal object CborStringSerializer : KSerializer, CborSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborString", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: CborString) { - encoder.asCborEncoder().encodeTags(value) + val cborEncoder = encoder.asCborEncoder() + cborEncoder.encodeTags(value) encoder.encodeString(value.value) } @@ -170,12 +196,13 @@ internal object CborStringSerializer : KSerializer { * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborBoolean]. * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). */ -internal object CborBooleanSerializer : KSerializer { +internal object CborBooleanSerializer : KSerializer, CborSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborBoolean", PrimitiveKind.BOOLEAN) override fun serialize(encoder: Encoder, value: CborBoolean) { - encoder.asCborEncoder().encodeTags(value) + val cborEncoder = encoder.asCborEncoder() + cborEncoder.encodeTags(value) encoder.encodeBoolean(value.value) } @@ -191,13 +218,13 @@ internal object CborBooleanSerializer : KSerializer { * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborByteString]. * It can only be used by with [Cbor] format and its input ([CborDecoder] and [CborEncoder]). */ -internal object CborByteStringSerializer : KSerializer { +internal object CborByteStringSerializer : KSerializer, CborSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborByteString", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: CborByteString) { val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value) + cborEncoder.encodeTags(value) cborEncoder.encodeByteString(value.value) } @@ -213,7 +240,7 @@ internal object CborByteStringSerializer : KSerializer { * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborMap]. * It can only be used by with [Cbor] format and its input ([CborDecoder] and [CborEncoder]). */ -internal object CborMapSerializer : KSerializer { +internal object CborMapSerializer : KSerializer, CborSerializer { private object CborMapDescriptor : SerialDescriptor by MapSerializer(CborElementSerializer, CborElementSerializer).descriptor { @ExperimentalSerializationApi @@ -238,7 +265,7 @@ internal object CborMapSerializer : KSerializer { * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborList]. * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). */ -internal object CborListSerializer : KSerializer { +internal object CborListSerializer : KSerializer, CborSerializer { private object CborListDescriptor : SerialDescriptor by ListSerializer(CborElementSerializer).descriptor { @ExperimentalSerializationApi override val serialName: String = "kotlinx.serialization.cbor.CborList" @@ -296,9 +323,7 @@ private fun defer(deferred: () -> SerialDescriptor): SerialDescriptor = object : private fun CborWriter.encodeTags(value: CborElement) { // Encode tags if present if (value.tags.isNotEmpty()) { - for (tag in value.tags) { - encodeTag(tag) - } + encodeTags(value.tags) } } \ No newline at end of file diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt index 8bb3401425..f3f4b2fbb2 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt @@ -6,6 +6,7 @@ package kotlinx.serialization.cbor.internal import kotlinx.serialization.* +import kotlinx.serialization.cbor.CborList /** * Common interface for CBOR parsers that can read CBOR data from different sources. @@ -38,4 +39,6 @@ internal sealed interface CborParserInterface { // Tag verification fun verifyTagsAndThrow(expected: ULongArray, actual: ULongArray?) + + fun processTags(tags: ULongArray?): ULongArray? } \ No newline at end of file diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt index c5fe746454..1deeaf78b5 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt @@ -28,20 +28,20 @@ internal class CborTreeReader( val result = when (parser.curByte shr 5) { // Get major type from the first 3 bits 0 -> { // Major type 0: unsigned integer val value = parser.nextNumber() - CborPositiveInt(value.toULong(), tags) + CborPositiveInt(value.toULong(), tags = tags) } 1 -> { // Major type 1: negative integer val value = parser.nextNumber() - CborNegativeInt(value, tags) + CborNegativeInt(value, tags = tags) } 2 -> { // Major type 2: byte string - CborByteString(parser.nextByteString(), tags) + CborByteString(parser.nextByteString(), tags = tags) } 3 -> { // Major type 3: text string - CborString(parser.nextString(), tags) + CborString(parser.nextString(), tags = tags) } 4 -> { // Major type 4: array @@ -56,27 +56,31 @@ internal class CborTreeReader( when (parser.curByte) { 0xF4 -> { parser.readByte() // Advance parser position - CborBoolean(false, tags) + CborBoolean(false, tags = tags) } 0xF5 -> { parser.readByte() // Advance parser position - CborBoolean(true, tags) + CborBoolean(true, tags = tags) } 0xF6, 0xF7 -> { parser.nextNull() - CborNull(tags) + CborNull(tags = tags) } // Half/Float32/Float64 - NEXT_HALF, NEXT_FLOAT, NEXT_DOUBLE -> CborDouble(parser.nextDouble(), tags) + NEXT_HALF, NEXT_FLOAT, NEXT_DOUBLE -> CborDouble(parser.nextDouble(), tags = tags) else -> throw CborDecodingException( - "Invalid simple value or float type: ${parser.curByte.toString(16)}" + "Invalid simple value or float type: ${parser.curByte.toString(16).uppercase()}" ) } } - else -> throw CborDecodingException("Invalid CBOR major type: ${parser.curByte shr 5}") + else -> { + val errByte = parser.curByte shr 5 + throw if (errByte == -1) CborDecodingException("Unexpected EOF") + else CborDecodingException("Invalid CBOR major type: $errByte") + } } return result } @@ -116,7 +120,7 @@ internal class CborTreeReader( parser.end() } - return CborList(elements, tags) + return CborList(elements, tags = tags) } private fun readMap(tags: ULongArray): CborMap { @@ -140,6 +144,6 @@ internal class CborTreeReader( parser.end() } - return CborMap(elements, tags) + return CborMap(elements, tags = tags) } } 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 cd35e5ad21..624b190864 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -18,7 +18,7 @@ internal open class CborReader(override val cbor: Cbor, protected val parser: Cb override fun decodeCborElement(): CborElement = when (parser) { is CborParser -> CborTreeReader(cbor.configuration, parser).read() - is StructuredCborParser -> parser.element + is StructuredCborParser -> parser.layer.current } @@ -116,7 +116,13 @@ internal open class CborReader(override val cbor: Cbor, protected val parser: Cb @OptIn(ExperimentalSerializationApi::class) override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { - return if ((decodeByteArrayAsByteString || cbor.configuration.alwaysUseByteString) + @Suppress("UNCHECKED_CAST") + return if (deserializer is CborSerializer) { + val tags = parser.processTags(tags) + decodeCborElement().also { /*this is a NOOP for structured parser but not from bytes */it.tags = + tags ?: ulongArrayOf() + } as T + } else if ((decodeByteArrayAsByteString || cbor.configuration.alwaysUseByteString) && deserializer.descriptor == ByteArraySerializer().descriptor ) { @Suppress("UNCHECKED_CAST") @@ -283,7 +289,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO input.readExactNBytes(strLen) } - private fun processTags(tags: ULongArray?): ULongArray? { + override fun processTags(tags: ULongArray?): ULongArray? { var index = 0 val collectedTags = mutableListOf() while ((curByte and 0b111_00000) == HEADER_TAG) { @@ -559,12 +565,15 @@ private fun Iterable.flatten(): ByteArray { * Works for single elements (where current is directly set to the element) and for collections (where current * will be first set after `startMap` or `startArray` */ -private class PeekingIterator(private val iter: ListIterator) : Iterator by iter { +internal class PeekingIterator private constructor( + internal val isStructure: Boolean, + private val iter: ListIterator +) : Iterator by iter { - lateinit var current: T + lateinit var current: CborElement private set - override fun next(): T = iter.next().also { current = it } + override fun next(): CborElement = iter.next().also { current = it } fun peek() = if (hasNext()) { val next = iter.next() @@ -573,36 +582,35 @@ private class PeekingIterator(private val iter: ListIterator) : Iter } else null companion object { - operator fun invoke(single: CborElement): PeekingIterator = - PeekingIterator(listOf(single).listIterator()).also { it.next() } + operator fun invoke(single: CborElement): PeekingIterator = + PeekingIterator(false, listOf(single).listIterator()).also { it.next() } + + operator fun invoke(iter: ListIterator): PeekingIterator = + PeekingIterator(true, iter) } } -/** Maps are a bit special for nullability checks, so we need to check whether we're in a map or not*/ -private typealias CborLayer = Pair> - -private val CborLayer.isMap get() = first - -/** current element reference inside a layer. If the layer is a primitive then the current element is the layer itself*/ -private val CborLayer.currentElement get() = second.current -private fun CborLayer.peek() = second.peek() -private fun CborLayer.hasNext() = second.hasNext() -private fun CborLayer.nextElement() = second.next() /** - * CBOR parser that operates on [CborElement] instead of bytes. Closely mirrors the baheviour of [CborParser], so the + * CBOR parser that operates on [CborElement] instead of bytes. Closely mirrors the behaviour of [CborParser], so the * [CborDecoder] can remain largely unchanged. */ internal class StructuredCborParser(internal val element: CborElement, private val verifyObjectTags: Boolean) : CborParserInterface { - private var layer: CborLayer = false to PeekingIterator(element) + internal var layer: PeekingIterator = PeekingIterator(element) + private set - private val layerStack = ArrayDeque() + private val layerStack = ArrayDeque() // map needs special treatment because keys and values are laid out as a list alternating between key and value to // mirror the byte-layout of a cbor map. - override fun isNull() = if (layer.isMap) layer.peek() is CborNull else layer.currentElement is CborNull + override fun isNull() = + if (layer.isStructure) layer.peek().let { + it is CborNull || + /*THIS IS NOT CBOR-COMPLIANT but KxS-proprietary handling of nullable classes*/ + (it is CborMap && it.isEmpty()) + } else layer.current is CborNull override fun isEnd() = !layer.hasNext() @@ -613,77 +621,80 @@ internal class StructuredCborParser(internal val element: CborElement, private v override fun startArray(tags: ULongArray?): Int { processTags(tags) - if (layer.currentElement !is CborList) { - throw CborDecodingException("Expected array, got ${layer.currentElement::class.simpleName}") + if (layer.current !is CborList) { + throw CborDecodingException("Expected array, got ${layer.current::class.simpleName}") } layerStack += layer - val list = layer.currentElement as CborList - layer = false to PeekingIterator(list.listIterator()) + val list = layer.current as CborList + layer = PeekingIterator(list.listIterator()) return list.size //we could just return -1 and let the current layer run out of elements to never run into inconsistencies // if we do keep it like this, any inconsistencies serve as a canary for implementation bugs } override fun startMap(tags: ULongArray?): Int { processTags(tags) - if (layer.currentElement !is CborMap) { - throw CborDecodingException("Expected map, got ${layer.currentElement::class.simpleName}") + if (layer.current !is CborMap) { + throw CborDecodingException("Expected map, got ${layer.current::class.simpleName}") } layerStack += layer - val map = layer.currentElement as CborMap + val map = layer.current as CborMap // zip key, value, key, value, ... pairs to mirror byte-layout of CBOR map, so decoding this here works the same // as decoding from bytes - layer = true to PeekingIterator(map.entries.flatMap { listOf(it.key, it.value) }.listIterator()) + layer = PeekingIterator(map.entries.flatMap { listOf(it.key, it.value) }.listIterator()) return map.size//we could just return -1 and let the current layer run out of elements to never run into inconsistencies // if we do keep it like this, any inconsistencies serve as a canary for implementation bugs } override fun nextNull(tags: ULongArray?): Nothing? { processTags(tags) - if (layer.currentElement !is CborNull) { - throw CborDecodingException("Expected null, got ${layer.currentElement::class.simpleName}") + if (layer.current !is CborNull) { + /* THIS IS NOT CBOR-COMPLIANT but KxS-proprietary handling of nullable classes*/ + if (layer.current is CborMap && (layer.current as CborMap).isEmpty()) + return null + throw CborDecodingException("Expected null, got ${layer.current::class.simpleName}") } return null } override fun nextBoolean(tags: ULongArray?): Boolean { processTags(tags) - if (layer.currentElement !is CborBoolean) { - throw CborDecodingException("Expected boolean, got ${layer.currentElement::class.simpleName}") + if (layer.current !is CborBoolean) { + throw CborDecodingException("Expected boolean, got ${layer.current::class.simpleName}") } - return (layer.currentElement as CborBoolean).value + return (layer.current as CborBoolean).value } override fun nextNumber(tags: ULongArray?): Long { processTags(tags) - return when (layer.currentElement) { - is CborPositiveInt -> (layer.currentElement as CborPositiveInt).value.toLong() - is CborNegativeInt -> (layer.currentElement as CborNegativeInt).value - else -> throw CborDecodingException("Expected number, got ${layer.currentElement::class.simpleName}") + return when (layer.current) { + is CborPositiveInt -> (layer.current as CborPositiveInt).value.toLong() + is CborNegativeInt -> (layer.current as CborNegativeInt).value + else -> throw CborDecodingException("Expected number, got ${layer.current::class.simpleName}") } } override fun nextString(tags: ULongArray?): String { processTags(tags) - if (layer.currentElement !is CborString) { - throw CborDecodingException("Expected string, got ${layer.currentElement::class.simpleName}") + if (layer.current !is CborString) { + throw CborDecodingException("Expected string, got ${layer.current::class.simpleName}") } - return (layer.currentElement as CborString).value + return (layer.current as CborString).value } override fun nextByteString(tags: ULongArray?): ByteArray { processTags(tags) - if (layer.currentElement !is CborByteString) { - throw CborDecodingException("Expected byte string, got ${layer.currentElement::class.simpleName}") + if (layer.current !is CborByteString) { + throw CborDecodingException("Expected byte string, got ${layer.current::class.simpleName}") } - return (layer.currentElement as CborByteString).value + return (layer.current as CborByteString).value } override fun nextDouble(tags: ULongArray?): Double { processTags(tags) - return when (layer.currentElement) { - is CborDouble -> (layer.currentElement as CborDouble).value - else -> throw CborDecodingException("Expected double, got ${layer.currentElement::class.simpleName}") + return when (layer.current) { + is CborDouble -> (layer.current as CborDouble).value + else -> throw CborDecodingException("Expected double, got ${layer.current::class.simpleName}") } } @@ -694,7 +705,7 @@ internal class StructuredCborParser(internal val element: CborElement, private v override fun nextTaggedStringOrNumber(): Triple { val tags = processTags(null) - return when (val key = layer.currentElement) { + return when (val key = layer.current) { is CborString -> Triple(key.value, null, tags) is CborPositiveInt -> Triple(null, key.value.toLong(), tags) is CborNegativeInt -> Triple(null, key.value, tags) @@ -707,14 +718,14 @@ internal class StructuredCborParser(internal val element: CborElement, private v * The reason this method mixes two behaviours is that decoding a primitive is invoked on a single element. * `decodeElementIndex`, etc. is invoked on an iterable and there are key tags and value tags */ - private fun processTags(tags: ULongArray?): ULongArray? { + override fun processTags(tags: ULongArray?): ULongArray? { // If we're in a list/map, advance to the next element - if (layer.hasNext()) layer.nextElement() + if (layer.hasNext()) layer.next() // if we're at a primitive, we only process tags // Store collected tags for verification - val collectedTags = if (layer.currentElement.tags.isEmpty()) null else layer.currentElement.tags + val collectedTags = if (layer.current.tags.isEmpty()) null else layer.current.tags // Verify tags if needed if (verifyObjectTags) { 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 c047142817..6cc67b2a21 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -38,7 +38,7 @@ internal sealed class CborWriter( class Data(val bytes: ByteArrayOutput, var elementCount: Int) - internal abstract fun getDestination(): ByteArrayOutput + protected abstract fun getDestination(): ByteArrayOutput override val serializersModule: SerializersModule get() = cbor.serializersModule @@ -148,7 +148,7 @@ internal sealed class CborWriter( return true } - internal fun encodeTag(tag: ULong) = getDestination().encodeTag(tag) + internal abstract fun encodeTags(tags: ULongArray) } @@ -183,6 +183,8 @@ internal class IndefiniteLengthCborWriter(cbor: Cbor, private val output: ByteAr override fun incrementChildren() {/*NOOP*/ } + override fun encodeTags(tags: ULongArray) = tags.forEach { getDestination().encodeTag(it) } + } // optimized indefinite length encoder @@ -235,11 +237,21 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { private val stack = ArrayDeque() private var currentElement: CborContainer? = null + // value tags are collects inside beginStructure, so we need to cache them here and write them in beginStructure or encodeXXX + // and then null them out, so there are no leftovers + private var nextValueTags: ULongArray = ulongArrayOf() + get() { + val ret = field + field = ulongArrayOf() + return ret + } + fun finalize() = currentElement!!.finalize() override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { - //TODO check if cborelement and be done - val tags = descriptor.getObjectTags() ?: ulongArrayOf() + val tags = nextValueTags + + if (cbor.configuration.encodeObjectTags) descriptor.getObjectTags() ?: ulongArrayOf() + else ulongArrayOf() val element = if (descriptor.hasArrayTag()) { CborContainer.List(tags) } else { @@ -262,7 +274,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { } } - override fun getDestination() = throw IllegalStateException("There is not byteArrayOutput") + override fun getDestination() = throw IllegalStateException("There is no byteArrayInput") override fun incrementChildren() { /*NOOP*/ @@ -270,13 +282,16 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean { - //we don't care for special encoding of an empty class, so we don't set this flag here - // isClass = descriptor.getElementDescriptor(index).kind == StructureKind.CLASS + // this mirrors the special encoding of nullable classes that are null into am empty map. + // THIS IS NOT CBOR-COMPLiANT + // but keeps backwards compatibility with the way kotlinx.serialization CBOR format has always worked. + isClass = descriptor.getElementDescriptor(index).kind == StructureKind.CLASS + encodeByteArrayAsByteString = descriptor.isByteString(index) //TODO check if cborelement and be done val name = descriptor.getElementName(index) if (!descriptor.hasArrayTag()) { - val keyTags = descriptor.getKeyTags(index) + val keyTags = if (cbor.configuration.encodeKeyTags) descriptor.getKeyTags(index) else null if ((descriptor.kind !is StructureKind.LIST) && (descriptor.kind !is StructureKind.MAP) && (descriptor.kind !is PolymorphicKind)) { //indices are put into the name field. we don't want to write those, as it would result in double writes @@ -284,66 +299,76 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { if (cbor.configuration.preferCborLabelsOverNames && cborLabel != null) { currentElement += CborInt(value = cborLabel, tags = keyTags ?: ulongArrayOf()) } else { - currentElement += CborString(name, keyTags ?: ulongArrayOf()) + currentElement += CborString(name, tags = keyTags ?: ulongArrayOf()) } } } if (cbor.configuration.encodeValueTags) { - descriptor.getValueTags(index)?.let { valueTags -> - currentElement!!.tags += valueTags + descriptor.getValueTags(index).let { valueTags -> + //collect them for late encoding in beginStructure or encodeXXX + nextValueTags = valueTags ?: ulongArrayOf() } } return true } + override fun encodeTags(tags: ULongArray) { + nextValueTags += tags + } + override fun encodeBoolean(value: Boolean) { - currentElement += CborBoolean(value) + currentElement += CborBoolean(value, tags = nextValueTags) } override fun encodeByte(value: Byte) { - currentElement += CborInt(value.toLong()) + currentElement += CborInt(value.toLong(), tags = nextValueTags) } override fun encodeChar(value: Char) { - currentElement += CborInt(value.code.toLong()) + currentElement += CborInt(value.code.toLong(), tags = nextValueTags) } override fun encodeDouble(value: Double) { - currentElement += CborDouble(value) + currentElement += CborDouble(value, tags = nextValueTags) } override fun encodeFloat(value: Float) { - currentElement += CborDouble(value.toDouble()) + currentElement += CborDouble(value.toDouble(), tags = nextValueTags) } override fun encodeInt(value: Int) { - currentElement += CborInt(value.toLong()) + currentElement += CborInt(value.toLong(), tags = nextValueTags) } override fun encodeLong(value: Long) { - currentElement += CborInt(value) + currentElement += CborInt(value, tags = nextValueTags) } override fun encodeShort(value: Short) { - currentElement += CborInt(value.toLong()) + currentElement += CborInt(value.toLong(), tags = nextValueTags) } override fun encodeString(value: String) { - currentElement += CborString(value) + currentElement += CborString(value, tags = nextValueTags) } override fun encodeByteString(byteArray: ByteArray) { - currentElement += CborByteString(byteArray) + currentElement += CborByteString(byteArray, tags = nextValueTags) } override fun encodeNull() { - currentElement += CborNull() + /*NOT CBOR-COMPLIANT, KxS-proprietary behaviour*/ + currentElement += if (isClass) CborMap( + mapOf(), + tags = nextValueTags + ) + else CborNull(tags = nextValueTags) } override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) { - currentElement += CborString(enumDescriptor.getElementName(index)) + currentElement += CborString(enumDescriptor.getElementName(index), tags = nextValueTags) } } @@ -360,6 +385,8 @@ internal class DefiniteLengthCborWriter(cbor: Cbor, output: ByteArrayOutput) : C structureStack.peek().elementCount++ } + override fun encodeTags(tags: ULongArray) = tags.forEach { getDestination().encodeTag(it) } + override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { val current = Data(ByteArrayOutput(), 0) val _ = structureStack.push(current) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborArrayTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborArrayTest.kt index 619c951e7e..c20bbcffdc 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborArrayTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborArrayTest.kt @@ -116,6 +116,8 @@ class CborArrayTest { assertEquals(reference, cbor.decodeFromHexString(DoubleTaggedClassWithArray.serializer(), referenceHexString)) val struct = cbor.encodeToCbor(DoubleTaggedClassWithArray.serializer(), reference) + val structFromHex = cbor.decodeFromHexString(CborElement.serializer(), referenceHexString) + assertEquals(structFromHex, struct) assertEquals(reference, cbor.decodeFromCbor(DoubleTaggedClassWithArray.serializer(), struct)) assertEquals(referenceHexString, cbor.encodeToHexString(CborElement.serializer(), struct)) } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt index d683acb2d4..73b282436c 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt @@ -130,18 +130,39 @@ class CborDecoderTest { @Test fun testIgnoreUnknownKeysFailsWhenCborDataIsMissingKeysThatArePresentInKotlinClass() { // with maps & lists of indefinite length + val hex = + "bf637374726d48656c6c6f2c20776f726c64216169182a686e756c6c61626c65f6646c6973749f61616162ff636d6170bf01f502f4ff65696e6e6572bf6161636c6f6cff6a696e6e6572734c6973749fbf6161636b656bffffff" + assertFailsWithMessage("Field 'a' is required") { ignoreUnknownKeys.decodeFromHexString( Simple.serializer(), - "bf637374726d48656c6c6f2c20776f726c64216169182a686e756c6c61626c65f6646c6973749f61616162ff636d6170bf01f502f4ff65696e6e6572bf6161636c6f6cff6a696e6e6572734c6973749fbf6161636b656bffffff" + hex + ) + } + + val struct = Cbor.decodeFromHexString(hex) + assertFailsWithMessage("Field 'a' is required") { + ignoreUnknownKeys.decodeFromCbor( + Simple.serializer(), + struct ) } // with maps & lists of definite length + val hexDef = + "a7646c6973748261616162686e756c6c61626c65f6636d6170a202f401f56169182a6a696e6e6572734c69737481a16161636b656b637374726d48656c6c6f2c20776f726c642165696e6e6572a16161636c6f6c" assertFailsWithMessage("Field 'a' is required") { ignoreUnknownKeys.decodeFromHexString( Simple.serializer(), - "a7646c6973748261616162686e756c6c61626c65f6636d6170a202f401f56169182a6a696e6e6572734c69737481a16161636b656b637374726d48656c6c6f2c20776f726c642165696e6e6572a16161636c6f6c" + hexDef + ) + } + + val structDef = Cbor.decodeFromHexString(hexDef) + assertFailsWithMessage("Field 'a' is required") { + ignoreUnknownKeys.decodeFromCbor( + Simple.serializer(), + structDef ) } } @@ -160,13 +181,19 @@ class CborDecoderTest { * 69676E6F7265 # "ignore" * (missing value associated with "ignore" key) */ - assertFailsWithMessage("Unexpected end of encoded CBOR document") { + val hex = "a36373747266737472696e676169006669676e6f7265" + assertFailsWithMessage("Unexpected EOF while skipping element") { ignoreUnknownKeys.decodeFromHexString( TypesUmbrella.serializer(), - "a36373747266737472696e676169006669676e6f7265" + hex ) } + + assertFailsWithMessage("Unexpected EOF") { + Cbor.decodeFromHexString(hex) + } + /* A3 # map(3) * 63 # text(3) * 737472 # "str" @@ -180,12 +207,17 @@ class CborDecoderTest { * A2 # map(2) * (missing map contents associated with "ignore" key) */ - assertFailsWithMessage("Unexpected end of encoded CBOR document") { + val hex2 = "a36373747266737472696e676169006669676e6f7265a2" + assertFailsWithMessage("Unexpected EOF while skipping element") { ignoreUnknownKeys.decodeFromHexString( TypesUmbrella.serializer(), - "a36373747266737472696e676169006669676e6f7265a2" + hex2 ) } + + assertFailsWithMessage("Unexpected EOF") { + Cbor.decodeFromHexString(hex2) + } } @Test @@ -199,12 +231,17 @@ class CborDecoderTest { * 69676E6F7265 # "ignore" * FF # primitive(*) */ + val hex = "a36373747266737472696e676669676e6f7265ff" assertFailsWithMessage("Expected next data item, but found FF") { ignoreUnknownKeys.decodeFromHexString( TypesUmbrella.serializer(), - "a36373747266737472696e676669676e6f7265ff" + hex ) } + + assertFailsWithMessage("Invalid simple value or float type: FF") { + Cbor.decodeFromHexString(hex) + } } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDefiniteLengthTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDefiniteLengthTest.kt index b19a409308..d43704d05f 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDefiniteLengthTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDefiniteLengthTest.kt @@ -26,6 +26,15 @@ class CborDefiniteLengthTest { "a9637374726d48656c6c6f2c20776f726c64216169182a686e756c6c61626c65f6646c6973748261616162636d6170a201f502f465696e6e6572a16161636c6f6c6a696e6e6572734c69737481a16161636b656b6a62797465537472696e6742cafe6962797465417272617982383521", Cbor { useDefiniteLengthEncoding = true }.encodeToHexString(TypesUmbrella.serializer(), test) ) + assertEquals( + "a9637374726d48656c6c6f2c20776f726c64216169182a686e756c6c61626c65f6646c6973748261616162636d6170a201f502f465696e6e6572a16161636c6f6c6a696e6e6572734c69737481a16161636b656b6a62797465537472696e6742cafe6962797465417272617982383521", + Cbor { useDefiniteLengthEncoding = true }.run { + encodeToHexString( + CborElement.serializer(), + encodeToCbor(TypesUmbrella.serializer(), test) + ) + } + ) } } \ No newline at end of file diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt index 08868ebc9e..911b8c6808 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt @@ -13,7 +13,7 @@ class CborElementEqualityTest { val int1 = CborPositiveInt(42u) val int2 = CborPositiveInt(42u) val int3 = CborPositiveInt(43u) - val int4 = CborPositiveInt(42u, ulongArrayOf(1u)) + val int4 = CborPositiveInt(42u, 1u) // Same values should be equal assertEquals(int1, int2) @@ -38,7 +38,7 @@ class CborElementEqualityTest { val int1 = CborNegativeInt(-42) val int2 = CborNegativeInt(-42) val int3 = CborNegativeInt(-43) - val int4 = CborNegativeInt(-42, ulongArrayOf(1u)) + val int4 = CborNegativeInt(-42, 1u) assertEquals(int1, int2) assertEquals(int1.hashCode(), int2.hashCode()) @@ -54,7 +54,7 @@ class CborElementEqualityTest { val double1 = CborDouble(3.14) val double2 = CborDouble(3.14) val double3 = CborDouble(2.71) - val double4 = CborDouble(3.14, ulongArrayOf(1u)) + val double4 = CborDouble(3.14, 1u) assertEquals(double1, double2) assertEquals(double1.hashCode(), double2.hashCode()) @@ -70,7 +70,7 @@ class CborElementEqualityTest { val string1 = CborString("hello") val string2 = CborString("hello") val string3 = CborString("world") - val string4 = CborString("hello", ulongArrayOf(1u)) + val string4 = CborString("hello", 1u) assertEquals(string1, string2) assertEquals(string1.hashCode(), string2.hashCode()) @@ -86,7 +86,7 @@ class CborElementEqualityTest { val bool1 = CborBoolean(true) val bool2 = CborBoolean(true) val bool3 = CborBoolean(false) - val bool4 = CborBoolean(true, ulongArrayOf(1u)) + val bool4 = CborBoolean(true, 1u) assertEquals(bool1, bool2) assertEquals(bool1.hashCode(), bool2.hashCode()) @@ -106,7 +106,7 @@ class CborElementEqualityTest { val byteString1 = CborByteString(bytes1) val byteString2 = CborByteString(bytes2) val byteString3 = CborByteString(bytes3) - val byteString4 = CborByteString(bytes1, ulongArrayOf(1u)) + val byteString4 = CborByteString(bytes1, 1u) assertEquals(byteString1, byteString2) assertEquals(byteString1.hashCode(), byteString2.hashCode()) @@ -121,7 +121,7 @@ class CborElementEqualityTest { fun testCborNullEquality() { val null1 = CborNull() val null2 = CborNull() - val null3 = CborNull(ulongArrayOf(1u)) + val null3 = CborNull(1u) assertEquals(null1, null2) assertEquals(null1.hashCode(), null2.hashCode()) @@ -136,7 +136,7 @@ class CborElementEqualityTest { val list1 = CborList(listOf(CborPositiveInt(1u), CborString("test"))) val list2 = CborList(listOf(CborPositiveInt(1u), CborString("test"))) val list3 = CborList(listOf(CborPositiveInt(2u), CborString("test"))) - val list4 = CborList(listOf(CborPositiveInt(1u), CborString("test")), ulongArrayOf(1u)) + val list4 = CborList(listOf(CborPositiveInt(1u), CborString("test")), 1u) val list5 = CborList(listOf(CborPositiveInt(1u))) assertEquals(list1, list2) @@ -151,25 +151,35 @@ class CborElementEqualityTest { @Test fun testCborMapEquality() { - val map1 = CborMap(mapOf( - CborString("key1") to CborPositiveInt(1u), - CborString("key2") to CborString("value") - )) - val map2 = CborMap(mapOf( - CborString("key1") to CborPositiveInt(1u), - CborString("key2") to CborString("value") - )) - val map3 = CborMap(mapOf( - CborString("key1") to CborPositiveInt(2u), - CborString("key2") to CborString("value") - )) - val map4 = CborMap(mapOf( - CborString("key1") to CborPositiveInt(1u), - CborString("key2") to CborString("value") - ), ulongArrayOf(1u)) - val map5 = CborMap(mapOf( - CborString("key1") to CborPositiveInt(1u) - )) + val map1 = CborMap( + mapOf( + CborString("key1") to CborPositiveInt(1u), + CborString("key2") to CborString("value") + ) + ) + val map2 = CborMap( + mapOf( + CborString("key1") to CborPositiveInt(1u), + CborString("key2") to CborString("value") + ) + ) + val map3 = CborMap( + mapOf( + CborString("key1") to CborPositiveInt(2u), + CborString("key2") to CborString("value") + ) + ) + val map4 = CborMap( + mapOf( + CborString("key1") to CborPositiveInt(1u), + CborString("key2") to CborString("value") + ), 1u + ) + val map5 = CborMap( + mapOf( + CborString("key1") to CborPositiveInt(1u) + ) + ) assertEquals(map1, map2) assertEquals(map1.hashCode(), map2.hashCode()) @@ -187,9 +197,9 @@ class CborElementEqualityTest { val tags2 = ulongArrayOf(1u, 2u, 3u) val tags3 = ulongArrayOf(1u, 2u, 4u) - val string1 = CborString("test", tags1) - val string2 = CborString("test", tags2) - val string3 = CborString("test", tags3) + val string1 = CborString("test", tags = tags1) + val string2 = CborString("test", tags = tags2) + val string3 = CborString("test", tags = tags3) assertEquals(string1, string2) assertEquals(string1.hashCode(), string2.hashCode()) @@ -213,24 +223,36 @@ class CborElementEqualityTest { @Test fun testNestedStructureEquality() { - val nested1 = CborMap(mapOf( - CborString("list") to CborList(listOf( - CborPositiveInt(1u), - CborMap(mapOf(CborString("inner") to CborNull())) - )) - )) - val nested2 = CborMap(mapOf( - CborString("list") to CborList(listOf( - CborPositiveInt(1u), - CborMap(mapOf(CborString("inner") to CborNull())) - )) - )) - val nested3 = CborMap(mapOf( - CborString("list") to CborList(listOf( - CborPositiveInt(2u), - CborMap(mapOf(CborString("inner") to CborNull())) - )) - )) + val nested1 = CborMap( + mapOf( + CborString("list") to CborList( + listOf( + CborPositiveInt(1u), + CborMap(mapOf(CborString("inner") to CborNull())) + ) + ) + ) + ) + val nested2 = CborMap( + mapOf( + CborString("list") to CborList( + listOf( + CborPositiveInt(1u), + CborMap(mapOf(CborString("inner") to CborNull())) + ) + ) + ) + ) + val nested3 = CborMap( + mapOf( + CborString("list") to CborList( + listOf( + CborPositiveInt(2u), + CborMap(mapOf(CborString("inner") to CborNull())) + ) + ) + ) + ) assertEquals(nested1, nested2) assertEquals(nested1.hashCode(), nested2.hashCode()) @@ -268,7 +290,13 @@ class CborElementEqualityTest { CborByteString(byteArrayOf(1, 2, 3)) to CborByteString(byteArrayOf(1, 2, 3)), CborNull() to CborNull(), CborList(listOf(CborPositiveInt(1u))) to CborList(listOf(CborPositiveInt(1u))), - CborMap(mapOf(CborString("key") to CborPositiveInt(1u))) to CborMap(mapOf(CborString("key") to CborPositiveInt(1u))) + CborMap(mapOf(CborString("key") to CborPositiveInt(1u))) to CborMap( + mapOf( + CborString("key") to CborPositiveInt( + 1u + ) + ) + ) ) pairs.forEach { (a, b) -> diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index 9cd9e937f9..d887995622 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -5,20 +5,13 @@ package kotlinx.serialization.cbor import kotlinx.serialization.* +import kotlinx.serialization.cbor.internal.* import kotlin.test.* class CborElementTest { private val cbor = Cbor {} - /** - * Helper method to decode a hex string to a CborElement - */ - private fun decodeHexToCborElement(hexString: String): CborElement { - val bytes = HexConverter.parseHexBinary(hexString.uppercase()) - return cbor.decodeFromByteArray(bytes) - } - @Test fun testCborNull() { val nullElement = CborNull() @@ -217,22 +210,21 @@ class CborElementTest { } @Test - fun testDecodeIntegers() { + fun testDecodePositiveInt() { // Test data from CborParserTest.testParseIntegers - val element = decodeHexToCborElement("0C") as CborPositiveInt + val element = cbor.decodeFromHexString("0C") as CborPositiveInt assertEquals(12u, element.value) - } @Test fun testDecodeStrings() { // Test data from CborParserTest.testParseStrings - val element = decodeHexToCborElement("6568656C6C6F") + val element = cbor.decodeFromHexString("6568656C6C6F") assertTrue(element is CborString) assertEquals("hello", element.value) val longStringElement = - decodeHexToCborElement("7828737472696E672074686174206973206C6F6E676572207468616E2032332063686172616374657273") + cbor.decodeFromHexString("7828737472696E672074686174206973206C6F6E676572207468616E2032332063686172616374657273") assertTrue(longStringElement is CborString) assertEquals("string that is longer than 23 characters", longStringElement.value) } @@ -240,11 +232,11 @@ class CborElementTest { @Test fun testDecodeFloatingPoint() { // Test data from CborParserTest.testParseDoubles - val doubleElement = decodeHexToCborElement("fb7e37e43c8800759c") + val doubleElement = cbor.decodeFromHexString("fb7e37e43c8800759c") assertTrue(doubleElement is CborDouble) assertEquals(1e+300, doubleElement.value) - val floatElement = decodeHexToCborElement("fa47c35000") + val floatElement = cbor.decodeFromHexString("fa47c35000") assertTrue(floatElement is CborDouble) assertEquals(100000.0f, floatElement.value.toFloat()) } @@ -252,7 +244,7 @@ class CborElementTest { @Test fun testDecodeByteString() { // Test data from CborParserTest.testRfc7049IndefiniteByteStringExample - val element = decodeHexToCborElement("5F44aabbccdd43eeff99FF") + val element = cbor.decodeFromHexString("5F44aabbccdd43eeff99FF") assertTrue(element is CborByteString) val byteString = element as CborByteString val expectedBytes = HexConverter.parseHexBinary("aabbccddeeff99") @@ -262,7 +254,7 @@ class CborElementTest { @Test fun testDecodeArray() { // Test data from CborParserTest.testSkipCollections - val element = decodeHexToCborElement("830118ff1a00010000") + val element = cbor.decodeFromHexString("830118ff1a00010000") assertTrue(element is CborList) val list = element as CborList assertEquals(3, list.size) @@ -274,7 +266,7 @@ class CborElementTest { @Test fun testDecodeMap() { // Test data from CborParserTest.testSkipCollections - val element = decodeHexToCborElement("a26178676b6f746c696e7861796d73657269616c697a6174696f6e") + val element = cbor.decodeFromHexString("a26178676b6f746c696e7861796d73657269616c697a6174696f6e") assertTrue(element is CborMap) val map = element as CborMap assertEquals(2, map.size) @@ -286,7 +278,7 @@ class CborElementTest { fun testDecodeComplexStructure() { // Test data from CborParserTest.testSkipIndefiniteLength val element = - decodeHexToCborElement("a461615f42cafe43010203ff61627f6648656c6c6f2065776f726c64ff61639f676b6f746c696e786d73657269616c697a6174696f6eff6164bf613101613202613303ff") + cbor.decodeFromHexString("a461615f42cafe43010203ff61627f6648656c6c6f2065776f726c64ff61639f676b6f746c696e786d73657269616c697a6174696f6eff6164bf613101613202613303ff") assertTrue(element is CborMap) val map = element as CborMap assertEquals(4, map.size) @@ -313,8 +305,6 @@ class CborElementTest { assertEquals(CborPositiveInt(3u), nestedMap[CborString("3")]) } - // Test removed due to incompatibility with the new tag implementation - @OptIn(ExperimentalStdlibApi::class) @Test fun testTagsRoundTrip() { @@ -329,11 +319,314 @@ class CborElementTest { // Verify the value and tags assertTrue(decodedElement is CborString) assertEquals("Hello, tagged world!", decodedElement.value) - assertNotNull(decodedElement.tags) assertEquals(1, decodedElement.tags.size) assertEquals(42u, decodedElement.tags.first()) } - + @Test + fun testGenericAndCborSpecificMixed() { + Triple( + Cbor { + encodeValueTags = true + encodeKeyTags = true + verifyKeyTags = true + verifyObjectTags = true + verifyValueTags = true + }, + MixedBag( + str = "A string, is a string, is a string", + bStr = CborByteString(byteArrayOf()), + cborElement = CborBoolean(false), + cborPositiveInt = CborPositiveInt(1u), + cborInt = CborInt(-1), + tagged = 26 + ), + "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472406b63626f72456c656d656e74f46f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564d90921181aff" + ) + .let { (cbor, obj, hex) -> + val struct = cbor.encodeToCbor(obj) + assertEquals(hex, cbor.encodeToHexString(obj)) + assertEquals(hex, cbor.encodeToHexString(struct)) + assertEquals(struct, cbor.decodeFromHexString(hex)) + assertEquals(obj, cbor.decodeFromCbor(struct)) + assertEquals(obj, cbor.decodeFromHexString(hex)) + } + + Triple( + Cbor { + encodeValueTags = true + encodeKeyTags = true + verifyKeyTags = true + verifyObjectTags = true + verifyValueTags = true + }, + MixedBag( + str = "A string, is a string, is a string", + bStr = null, + cborElement = CborBoolean(false), + cborPositiveInt = CborPositiveInt(1u), + cborInt = CborInt(-1), + tagged = 26 + ), + "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472f66b63626f72456c656d656e74f46f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564d90921181aff" + ) + .let { (cbor, obj, hex) -> + val struct = cbor.encodeToCbor(obj) + assertEquals(hex, cbor.encodeToHexString(obj)) + assertEquals(hex, cbor.encodeToHexString(struct)) + assertEquals(struct, cbor.decodeFromHexString(hex)) + assertEquals(obj, cbor.decodeFromCbor(struct)) + assertEquals(obj, cbor.decodeFromHexString(hex)) + } + + + Triple( + Cbor { + encodeValueTags = true + encodeKeyTags = true + verifyKeyTags = true + verifyObjectTags = true + verifyValueTags = true + }, + MixedBag( + str = "A string, is a string, is a string", + bStr = null, + cborElement = CborMap(mapOf(CborByteString(byteArrayOf(1, 3, 3, 7)) to CborNull())), + cborPositiveInt = CborPositiveInt(1u), + cborInt = CborInt(-1), + tagged = 26 + ), + "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472f66b63626f72456c656d656e74bf4401030307f6ff6f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564d90921181aff" + ) + .let { (cbor, obj, hex) -> + val struct = cbor.encodeToCbor(obj) + assertEquals(hex, cbor.encodeToHexString(obj)) + assertEquals(hex, cbor.encodeToHexString(struct)) + assertEquals(struct, cbor.decodeFromHexString(hex)) + assertEquals(obj, cbor.decodeFromCbor(struct)) + assertEquals(obj, cbor.decodeFromHexString(hex)) + } + + + + Triple( + Cbor { + encodeValueTags = true + encodeKeyTags = true + verifyKeyTags = true + verifyObjectTags = true + verifyValueTags = true + }, + MixedBag( + str = "A string, is a string, is a string", + bStr = null, + cborElement = CborNull(), + cborPositiveInt = CborPositiveInt(1u), + cborInt = CborInt(-1), + tagged = 26 + ), + "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472f66b63626f72456c656d656e74f66f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564d90921181aff" + ) + .let { (cbor, obj, hex) -> + val struct = cbor.encodeToCbor(obj) + assertEquals(hex, cbor.encodeToHexString(obj)) + assertEquals(hex, cbor.encodeToHexString(struct)) + assertEquals(struct, cbor.decodeFromHexString(hex)) + //we have an ambiguity here (null vs. CborNull), so we cannot compare for equality with the object + //assertEquals(obj, cbor.decodeFromCbor(struct)) + //assertEquals(obj, cbor.decodeFromHexString(hex)) + } + + Triple( + Cbor { + encodeValueTags = true + encodeKeyTags = true + verifyKeyTags = true + verifyObjectTags = true + verifyValueTags = true + }, + MixedBag( + str = "A string, is a string, is a string", + bStr = CborByteString(byteArrayOf(), 1u, 3u), + cborElement = CborBoolean(false), + cborPositiveInt = CborPositiveInt(1u), + cborInt = CborInt(-1), + tagged = 26 + ), + "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472c1c3406b63626f72456c656d656e74f46f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564d90921181aff" + ) + .let { (cbor, obj, hex) -> + val struct = cbor.encodeToCbor(obj) + assertEquals(hex, cbor.encodeToHexString(obj)) + assertEquals(hex, cbor.encodeToHexString(struct)) + assertEquals(struct, cbor.decodeFromHexString(hex)) + assertEquals(obj, cbor.decodeFromCbor(struct)) + assertEquals(obj, cbor.decodeFromHexString(hex)) + } + + Triple( + Cbor { + encodeValueTags = false + encodeKeyTags = true + verifyKeyTags = true + verifyObjectTags = true + verifyValueTags = false + }, + MixedBag( + str = "A string, is a string, is a string", + bStr = CborByteString(byteArrayOf(), 1u, 3u), + cborElement = CborBoolean(false), + cborPositiveInt = CborPositiveInt(1u), + cborInt = CborInt(-1), + tagged = 26 + ), + "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472c1c3406b63626f72456c656d656e74f46f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564181aff" + ) + .let { (cbor, obj, hex) -> + val struct = cbor.encodeToCbor(obj) + assertEquals(hex, cbor.encodeToHexString(obj)) + assertEquals(hex, cbor.encodeToHexString(struct)) + assertEquals(struct, cbor.decodeFromHexString(hex)) + assertEquals(obj, cbor.decodeFromCbor(struct)) + assertEquals(obj, cbor.decodeFromHexString(hex)) + } + + Triple( + Cbor { + encodeValueTags = true + encodeKeyTags = true + verifyKeyTags = true + verifyObjectTags = true + verifyValueTags = true + }, + MixedTag( + cborElement = CborBoolean(false), + ), + "bfd82a6b63626f72456c656d656e74d90921f4ff" + ) + .let { (cbor, obj, hex) -> + val struct = cbor.encodeToCbor(obj) + assertEquals(hex, cbor.encodeToHexString(obj)) + assertEquals(hex, cbor.encodeToHexString(struct)) + assertEquals(struct, cbor.decodeFromHexString(hex)) + // this is ambiguous. the cborBoolean has a tag attached in the struct, coming from the valueTag (as intended), + // so now the resulting object won't have a tag but the cborElement property will have a tag attached + // hence, the following two will have: + // Expected :MixedTag(cborElement=CborPrimitive(kind=Boolean, tags=, value=false)) + // Actual :MixedTag(cborElement=CborPrimitive(kind=Boolean, tags=2337, value=false)) + assertNotEquals(obj, cbor.decodeFromCbor(struct)) + assertNotEquals(obj, cbor.decodeFromHexString(hex)) + } + Triple( + Cbor { + encodeValueTags = true + encodeKeyTags = true + verifyKeyTags = true + verifyObjectTags = true + verifyValueTags = true + }, + MixedTag( + cborElement = CborBoolean(false, 90u), + ), + //valueTags first, then CborElement tags + "bfd82a6b63626f72456c656d656e74d90921d85af4ff" + ) + .let { (cbor, obj, hex) -> + val struct = cbor.encodeToCbor(obj) + assertEquals(hex, cbor.encodeToHexString(obj)) + assertEquals(hex, cbor.encodeToHexString(struct)) + assertEquals(struct, cbor.decodeFromHexString(hex)) + // this is ambiguous. the cborBoolean has a tag attached in the struct, coming from the valueTag (as intended), + // so now the resulting object won't have a tag but the cborElement property will have a tag attached + // hence, the following two will have: + // Expected :MixedTag(cborElement=CborPrimitive(kind=Boolean, tags=90, value=false)) + // Actual :MixedTag(cborElement=CborPrimitive(kind=Boolean, tags=2337, 90, value=false)) + //of course, the value tag verification will also fail hard + assertFailsWith( + CborDecodingException::class, + "CBOR tags [2337, 90] do not match expected tags [2337]" + ) { + assertNotEquals(obj, cbor.decodeFromCbor(struct)) + } + assertFailsWith( + CborDecodingException::class, + "CBOR tags [2337, 90] do not match expected tags [2337]" + ) { + assertNotEquals(obj, cbor.decodeFromHexString(hex)) + } + } + + Triple( + Cbor { + encodeValueTags = true + encodeKeyTags = true + verifyKeyTags = true + verifyObjectTags = true + verifyValueTags = false + }, + MixedTag( + cborElement = CborBoolean(false, 90u), + ), + //valueTags first, then CborElement tags + "bfd82a6b63626f72456c656d656e74d90921d85af4ff" + ) + .let { (cbor, obj, hex) -> + val struct = cbor.encodeToCbor(obj) + assertEquals(hex, cbor.encodeToHexString(obj)) + assertEquals(hex, cbor.encodeToHexString(struct)) + assertEquals(struct, cbor.decodeFromHexString(hex)) + // this is ambiguous. the cborBoolean has a tag attached in the struct, coming from the valueTag (as intended), + // so now the resulting object won't have a tag but the cborElement property will have a tag attached + // hence, the following two will have: + // Expected :MixedTag(cborElement=CborPrimitive(kind=Boolean, tags=90, value=false)) + // Actual :MixedTag(cborElement=CborPrimitive(kind=Boolean, tags=2337, 90, value=false)) + assertNotEquals(obj, cbor.decodeFromCbor(struct)) + assertNotEquals(obj, cbor.decodeFromHexString(hex)) + } + + + Triple( + Cbor { + encodeValueTags = false + encodeKeyTags = true + verifyKeyTags = true + verifyObjectTags = true + verifyValueTags = false + }, + MixedTag( + cborElement = CborBoolean(false, 90u), + ), + "bfd82a6b63626f72456c656d656e74d85af4ff" + ) + .let { (cbor, obj, hex) -> + val struct = cbor.encodeToCbor(obj) + assertEquals(hex, cbor.encodeToHexString(obj)) + assertEquals(hex, cbor.encodeToHexString(struct)) + assertEquals(struct, cbor.decodeFromHexString(hex)) + //no value tags means everything's fine again + assertEquals(obj, cbor.decodeFromCbor(struct)) + assertEquals(obj, cbor.decodeFromHexString(hex)) + } + } } + +@Serializable +data class MixedBag( + val str: String, + val bStr: CborByteString?, + val cborElement: CborElement?, + val cborPositiveInt: CborPrimitive<*>, + val cborInt: CborInt<*>, + @KeyTags(42u) + @ValueTags(2337u) + val tagged: Int +) + + +@Serializable +data class MixedTag( + @KeyTags(42u) + @ValueTags(2337u) + val cborElement: CborElement?, +) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborLabelTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborLabelTest.kt index 0ecb5283db..109376511c 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborLabelTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborLabelTest.kt @@ -35,6 +35,10 @@ class CborLabelTest { } assertEquals(referenceHexLabelString, cbor.encodeToHexString(ClassWithCborLabel.serializer(), reference)) assertEquals(reference, cbor.decodeFromHexString(ClassWithCborLabel.serializer(), referenceHexLabelString)) + + val struct = cbor.encodeToCbor(ClassWithCborLabel.serializer(), reference) + assertEquals(reference, cbor.decodeFromCbor(ClassWithCborLabel.serializer(), struct)) + assertEquals(referenceHexLabelString, cbor.encodeToHexString(CborElement.serializer(), struct)) } @Test @@ -44,6 +48,11 @@ class CborLabelTest { } assertEquals(referenceHexNameString, cbor.encodeToHexString(ClassWithCborLabel.serializer(), reference)) assertEquals(reference, cbor.decodeFromHexString(ClassWithCborLabel.serializer(), referenceHexNameString)) + + + val struct = cbor.encodeToCbor(ClassWithCborLabel.serializer(), reference) + assertEquals(reference, cbor.decodeFromCbor(ClassWithCborLabel.serializer(), struct)) + assertEquals(referenceHexNameString, cbor.encodeToHexString(CborElement.serializer(), struct)) } @Test @@ -64,6 +73,10 @@ class CborLabelTest { } assertEquals(referenceHexLabelWithTagString, cbor.encodeToHexString(ClassWithCborLabelAndTag.serializer(), referenceWithTag)) assertEquals(referenceWithTag, cbor.decodeFromHexString(ClassWithCborLabelAndTag.serializer(), referenceHexLabelWithTagString)) + + val struct = cbor.encodeToCbor(ClassWithCborLabelAndTag.serializer(), referenceWithTag) + assertEquals(referenceWithTag, cbor.decodeFromCbor(ClassWithCborLabelAndTag.serializer(), struct)) + assertEquals(referenceHexLabelWithTagString, cbor.encodeToHexString(CborElement.serializer(), struct)) } @Test @@ -84,6 +97,15 @@ class CborLabelTest { assertFailsWith(CborDecodingException::class) { cbor.decodeFromHexString(ClassWithCborLabelAndTag.serializer(), referenceHexLabelWithTagString) } + + //we can encode and decode since it is a valid structure + val struct = cbor.decodeFromHexString(CborElement.serializer(), referenceHexLabelWithTagString) + assertEquals(referenceHexLabelWithTagString, cbor.encodeToHexString(CborElement.serializer(), struct)) + + //we cannot deserialize from the struct since it does not match the class structure + assertFailsWith(CborDecodingException::class) { + cbor.decodeFromCbor(ClassWithCborLabelAndTag.serializer(), struct) + } } @Test @@ -107,6 +129,18 @@ class CborLabelTest { useDefiniteLengthEncoding = true } assertEquals(referenceWithTag, cbor.decodeFromHexString(ClassWithCborLabelAndTag.serializer(), referenceHexLabelWithTagString)) + + //no unknown props + val struct = cbor.encodeToCbor(ClassWithCborLabelAndTag.serializer(), referenceWithTag) + + //with unknown props + val structFromString = cbor.decodeFromHexString(CborElement.serializer(), referenceHexLabelWithTagString) + //must obv mismatch + assertNotEquals(struct, structFromString) + assertNotEquals(referenceHexLabelWithTagString, cbor.encodeToHexString(CborElement.serializer(), struct)) + + assertEquals(referenceWithTag, cbor.decodeFromCbor(ClassWithCborLabelAndTag.serializer(), struct)) + assertEquals(referenceHexLabelWithTagString, cbor.encodeToHexString(CborElement.serializer(), structFromString)) } @Test @@ -128,6 +162,11 @@ class CborLabelTest { } assertEquals(referenceWithoutLabel, cbor.decodeFromHexString(ClassWithoutCborLabel.serializer(), referenceHexStringWithoutLabel)) + + val struct = cbor.encodeToCbor(ClassWithoutCborLabel.serializer(), referenceWithoutLabel) + assertEquals(referenceWithoutLabel, cbor.decodeFromCbor(ClassWithoutCborLabel.serializer(), struct)) + assertEquals(referenceHexStringWithoutLabel, cbor.encodeToHexString(CborElement.serializer(), struct)) + } @Serializable diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborPolymorphismTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborPolymorphismTest.kt index 156f471480..bc033173eb 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborPolymorphismTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborPolymorphismTest.kt @@ -18,12 +18,20 @@ class CborPolymorphismTest { @Test fun testSealedWithOneSubclass() { + val original = A.B("bbb") + val hexResultToCheck = + "9f78336b6f746c696e782e73657269616c697a6174696f6e2e63626f722e43626f72506f6c796d6f72706869736d546573742e412e42bf616263626262ffff" assertSerializedToBinaryAndRestored( - A.B("bbb"), + original, A.serializer(), cbor, - hexResultToCheck = "9f78336b6f746c696e782e73657269616c697a6174696f6e2e63626f722e43626f72506f6c796d6f72706869736d546573742e412e42bf616263626262ffff" + hexResultToCheck = hexResultToCheck ) + + val struct = cbor.encodeToCbor(A.serializer(), original) + assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hexResultToCheck)) + assertEquals(hexResultToCheck, cbor.encodeToHexString(struct)) + assertEquals(original, cbor.decodeFromCbor(A.serializer(), struct)) } @Test @@ -35,6 +43,9 @@ class CborPolymorphismTest { ) ) assertSerializedToBinaryAndRestored(obj, SealedBox.serializer(), cbor) + + val struct = cbor.encodeToCbor(SealedBox.serializer(), obj) + assertEquals(obj, cbor.decodeFromCbor(SealedBox.serializer(), struct)) } @Test @@ -46,5 +57,9 @@ class CborPolymorphismTest { ) ) assertSerializedToBinaryAndRestored(obj, PolyBox.serializer(), cbor) + + + val struct = cbor.encodeToCbor(PolyBox.serializer(), obj) + assertEquals(obj, cbor.decodeFromCbor(PolyBox.serializer(), struct)) } } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborTaggedTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborTaggedTest.kt index babe9e63d3..298e38a039 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborTaggedTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborTaggedTest.kt @@ -223,7 +223,7 @@ class CborTaggedTest { @Test fun writeReadVerifyTaggedClass() { - assertEquals(referenceHexString, Cbor { + val cbor = Cbor { useDefiniteLengthEncoding = false encodeKeyTags = true encodeValueTags = true @@ -231,77 +231,119 @@ class CborTaggedTest { verifyKeyTags = true verifyValueTags = true verifyObjectTags = true - }.encodeToHexString(DataWithTags.serializer(), reference)) - assertEquals( - referenceHexStringDefLen, - Cbor { - useDefiniteLengthEncoding = true - encodeKeyTags = true - encodeValueTags = true - encodeObjectTags = true - verifyKeyTags = true - verifyValueTags = true - verifyObjectTags = true - }.encodeToHexString(DataWithTags.serializer(), reference) - ) + } + assertEquals(referenceHexString, cbor.encodeToHexString(DataWithTags.serializer(), reference)) + val structFromHex = cbor.decodeFromHexString(CborElement.serializer(), referenceHexString) + val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + assertEquals(struct, structFromHex) + assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + assertEquals(referenceHexString, cbor.encodeToHexString(CborElement.serializer(), struct)) + + val cborDef = Cbor { + useDefiniteLengthEncoding = true + encodeKeyTags = true + encodeValueTags = true + encodeObjectTags = true + verifyKeyTags = true + verifyValueTags = true + verifyObjectTags = true + } + assertEquals(referenceHexStringDefLen, cborDef.encodeToHexString(DataWithTags.serializer(), reference)) + val structDefFromHex = cborDef.decodeFromHexString(CborElement.serializer(), referenceHexStringDefLen) + val structDef = cborDef.encodeToCbor(DataWithTags.serializer(), reference) + assertEquals(structDef, structDefFromHex) + assertEquals(reference, cborDef.decodeFromCbor(DataWithTags.serializer(), structDef)) + assertEquals(referenceHexStringDefLen, cborDef.encodeToHexString(CborElement.serializer(), structDef)) + + assertEquals(reference, Cbor.CoseCompliant.decodeFromHexString(DataWithTags.serializer(), referenceHexString)) + val structCoseFromHex = Cbor.CoseCompliant.decodeFromHexString(CborElement.serializer(), referenceHexString) + val structCose = Cbor.CoseCompliant.encodeToCbor(DataWithTags.serializer(), reference) + assertEquals(structCose, structCoseFromHex) + assertEquals(reference, Cbor.CoseCompliant.decodeFromCbor(DataWithTags.serializer(), structCose)) + assertEquals( reference, Cbor.CoseCompliant.decodeFromHexString(DataWithTags.serializer(), referenceHexStringDefLen) ) + val structCoseFromHexDef = + Cbor.CoseCompliant.decodeFromHexString(CborElement.serializer(), referenceHexStringDefLen) + val structCoseDef = Cbor.CoseCompliant.encodeToCbor(DataWithTags.serializer(), reference) + assertEquals(structCoseDef, structCoseFromHexDef) + assertEquals(reference, Cbor.CoseCompliant.decodeFromCbor(DataWithTags.serializer(), structCoseDef)) + } @Test fun writeReadUntaggedKeys() { - assertEquals(noKeyTags, Cbor { + val cborNoKeyTags = Cbor { encodeKeyTags = false encodeValueTags = true encodeObjectTags = true verifyKeyTags = false verifyValueTags = true verifyObjectTags = true - }.encodeToHexString(DataWithTags.serializer(), reference)) - assertEquals( - noKeyTagsDefLen, - Cbor { - useDefiniteLengthEncoding = true - encodeKeyTags = false - encodeValueTags = true - encodeObjectTags = true - verifyKeyTags = true - verifyValueTags = true - verifyObjectTags = true - }.encodeToHexString( - DataWithTags.serializer(), - reference - ) - ) - assertEquals(reference, Cbor { - encodeKeyTags = true - encodeValueTags = true - encodeObjectTags = true - verifyValueTags = true - verifyObjectTags = true - verifyKeyTags = false - }.decodeFromHexString(noKeyTags)) - assertEquals(reference, Cbor { - encodeKeyTags = true + } + assertEquals(noKeyTags, cborNoKeyTags.encodeToHexString(DataWithTags.serializer(), reference)) + (cborNoKeyTags to noKeyTags).let { (cbor, hex) -> + val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + } + + val cborNoKeyTagsDefLen = Cbor { + useDefiniteLengthEncoding = true + encodeKeyTags = false encodeValueTags = true encodeObjectTags = true + verifyKeyTags = true verifyValueTags = true verifyObjectTags = true - verifyKeyTags = false - }.decodeFromHexString(noKeyTagsDefLen)) - assertEquals(reference, Cbor { + } + assertEquals(noKeyTagsDefLen, cborNoKeyTagsDefLen.encodeToHexString(DataWithTags.serializer(), reference)) + (cborNoKeyTagsDefLen to noKeyTagsDefLen).let { (cbor, hex) -> + val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + // this must fail, because encoding/decoding is not symmetric with the current config (the struct does not have the tags, but the hex string does) + assertFailsWith(CborDecodingException::class) { + assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + } + } + + val cborEncodingKeyTags = Cbor { encodeKeyTags = true encodeValueTags = true encodeObjectTags = true verifyValueTags = true verifyObjectTags = true verifyKeyTags = false - }.decodeFromHexString(referenceHexString)) + } + assertEquals(reference, cborEncodingKeyTags.decodeFromHexString(noKeyTags)) + (cborEncodingKeyTags to noKeyTags).let { (cbor, hex) -> + val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + // this must not be equal, because the scruct has the tags, but the hex string doesn't + assertNotEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + } + + assertEquals(reference, cborEncodingKeyTags.decodeFromHexString(noKeyTagsDefLen)) + (cborEncodingKeyTags to noKeyTagsDefLen).let { (cbor, hex) -> + val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + // this must not be equals, because the scruct has the tags, but the hex string doesn't (as above) + assertNotEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + } + + + assertEquals(reference, cborEncodingKeyTags.decodeFromHexString(referenceHexString)) + (cborNoKeyTags to noKeyTags).let { (cbor, hex) -> + val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + } assertFailsWith(CborDecodingException::class) { + //Tested against struct inside one of the above let-blocks Cbor.CoseCompliant.decodeFromHexString( DataWithTags.serializer(), noKeyTags @@ -309,46 +351,55 @@ class CborTaggedTest { } assertFailsWith(CborDecodingException::class) { - Cbor { - encodeKeyTags = true - encodeValueTags = true - encodeObjectTags = true - verifyValueTags = true - verifyObjectTags = true - verifyKeyTags = false - }.decodeFromHexString(DataWithTags.serializer(), noValueTags) + //Tested against struct inside one of the above let-blocks + cborEncodingKeyTags.decodeFromHexString(DataWithTags.serializer(), noValueTags) } } @Test fun writeReadUntaggedValues() { - assertEquals( - noValueTags, - Cbor { - encodeKeyTags = true - encodeObjectTags = true - verifyKeyTags = true - verifyValueTags = true - verifyObjectTags = true - encodeValueTags = false - }.encodeToHexString(DataWithTags.serializer(), reference) - ) - assertEquals(reference, Cbor { + val cborNoValueTags = Cbor { encodeKeyTags = true - encodeValueTags = true encodeObjectTags = true verifyKeyTags = true + verifyValueTags = true verifyObjectTags = true - verifyValueTags = false - }.decodeFromHexString(noValueTags)) - assertEquals(reference, Cbor { + encodeValueTags = false + } + assertEquals(noValueTags, cborNoValueTags.encodeToHexString(DataWithTags.serializer(), reference)) + (cborNoValueTags to noValueTags).let { (cbor, hex) -> + val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + // no value tags are written to the struct, so this will fail + assertFailsWith(CborDecodingException::class) { + assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + } + } + + + val cborEncodingValueTags = Cbor { encodeKeyTags = true encodeValueTags = true encodeObjectTags = true verifyKeyTags = true verifyObjectTags = true verifyValueTags = false - }.decodeFromHexString(referenceHexString)) + } + assertEquals(reference, cborEncodingValueTags.decodeFromHexString(noValueTags)) + (cborEncodingValueTags to noValueTags).let { (cbor, hex) -> + val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + //hex is missing the tags, struct has them from the serializer + assertNotEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + } + + + assertEquals(reference, cborEncodingValueTags.decodeFromHexString(referenceHexString)) + (cborEncodingValueTags to referenceHexString).let { (cbor, hex) -> + val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + } assertFailsWith(CborDecodingException::class) { Cbor { @@ -362,79 +413,84 @@ class CborTaggedTest { DataWithTags.serializer(), noValueTags ) + //Struct stuff has been tested in the above let blocks already } assertFailsWith(CborDecodingException::class) { - Cbor { - encodeKeyTags = true - encodeValueTags = true - encodeObjectTags = true - verifyKeyTags = true - verifyObjectTags = true - verifyValueTags = false - }.decodeFromHexString( + cborEncodingValueTags.decodeFromHexString( DataWithTags.serializer(), noKeyTags ) + //Struct stuff has been tested already } } @Test fun writeReadUntaggedEverything() { - assertEquals( - noTags, - Cbor { - encodeObjectTags = true - verifyKeyTags = true - verifyValueTags = true - verifyObjectTags = true - encodeValueTags = false - encodeKeyTags = false - }.encodeToHexString(DataWithTags.serializer(), reference) - ) - assertEquals( - noTagsDefLen, - Cbor { - encodeObjectTags = true - verifyKeyTags = true - verifyValueTags = true - verifyObjectTags = true - encodeValueTags = false - encodeKeyTags = false - useDefiniteLengthEncoding = true - }.encodeToHexString(DataWithTags.serializer(), reference) - ) - - assertEquals(reference, Cbor { - encodeKeyTags = true - encodeValueTags = true + val cborNoTags = Cbor { encodeObjectTags = true + verifyKeyTags = true + verifyValueTags = true verifyObjectTags = true - verifyKeyTags = false - verifyValueTags = false - }.decodeFromHexString(noTags)) + encodeValueTags = false + encodeKeyTags = false + } + assertEquals(noTags, cborNoTags.encodeToHexString(DataWithTags.serializer(), reference)) + (cborNoTags to noTags).let { (cbor, hex) -> + val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + //struct is missing the tags + assertFailsWith(CborDecodingException::class) { + assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + } + } - assertEquals(reference, Cbor { - encodeKeyTags = true - encodeValueTags = true + val cborNoTagsDefLen = Cbor { encodeObjectTags = true + verifyKeyTags = true + verifyValueTags = true verifyObjectTags = true - verifyKeyTags = false - verifyValueTags = false - }.decodeFromHexString(noTagsDefLen)) + encodeValueTags = false + encodeKeyTags = false + useDefiniteLengthEncoding = true + } + assertEquals(noTagsDefLen, cborNoTagsDefLen.encodeToHexString(DataWithTags.serializer(), reference)) + (cborNoTagsDefLen to noTagsDefLen).let { (cbor, hex) -> + val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + //struct is missing the tags + assertFailsWith(CborDecodingException::class) { + assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + } + } - assertEquals(reference, Cbor { + val cborEncodingAllVerifyingObjectTags = Cbor { encodeKeyTags = true encodeValueTags = true encodeObjectTags = true verifyObjectTags = true verifyKeyTags = false verifyValueTags = false - useDefiniteLengthEncoding = true - }.decodeFromHexString(noTags)) + } + assertEquals(reference, cborEncodingAllVerifyingObjectTags.decodeFromHexString(noTags)) + (cborEncodingAllVerifyingObjectTags to noTags).let { (cbor, hex) -> + val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + //hex has not tags but the current config encodes them into the struct + assertNotEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + } + + assertEquals(reference, cborEncodingAllVerifyingObjectTags.decodeFromHexString(noTagsDefLen)) + (cborEncodingAllVerifyingObjectTags to noTagsDefLen).let { (cbor, hex) -> + val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + //hex has not tags but the current config encodes them into the struct + assertNotEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + } + - assertEquals(reference, Cbor { + val cborEncodingAllDefLen = Cbor { encodeKeyTags = true encodeValueTags = true encodeObjectTags = true @@ -442,34 +498,45 @@ class CborTaggedTest { verifyKeyTags = false verifyValueTags = false useDefiniteLengthEncoding = true - }.decodeFromHexString(noTagsDefLen)) + } + assertEquals(reference, cborEncodingAllDefLen.decodeFromHexString(noTags)) + (cborEncodingAllDefLen to noTags).let { (cbor, hex) -> + val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + //hex has not tags but the current config encodes them into the struct + assertNotEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + } - assertEquals(reference, Cbor { - encodeKeyTags = true - encodeValueTags = true - encodeObjectTags = true - verifyObjectTags = true - verifyKeyTags = false - verifyValueTags = false - }.decodeFromHexString(noKeyTags)) + assertEquals(reference, cborEncodingAllDefLen.decodeFromHexString(noTagsDefLen)) + (cborEncodingAllDefLen to noTagsDefLen).let { (cbor, hex) -> + val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + //hex has not tags but the current config encodes them into the struct + assertNotEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + } - assertEquals(reference, Cbor { - encodeKeyTags = true - encodeValueTags = true - encodeObjectTags = true - verifyObjectTags = true - verifyKeyTags = false - verifyValueTags = false - }.decodeFromHexString(noValueTags)) + assertEquals(reference, cborEncodingAllVerifyingObjectTags.decodeFromHexString(noKeyTags)) + (cborEncodingAllVerifyingObjectTags to noKeyTags).let { (cbor, hex) -> + val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + //hex has not tags but the current config encodes them into the struct + assertNotEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + } - assertEquals(reference, Cbor { - encodeKeyTags = true - encodeValueTags = true - encodeObjectTags = true - verifyObjectTags = true - verifyKeyTags = false - verifyValueTags = false - }.decodeFromHexString(referenceHexString)) + assertEquals(reference, cborEncodingAllVerifyingObjectTags.decodeFromHexString(noValueTags)) + (cborEncodingAllVerifyingObjectTags to noValueTags).let { (cbor, hex) -> + val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + //hex has not tags but the current config encodes them into the struct + assertNotEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + } + + assertEquals(reference, cborEncodingAllVerifyingObjectTags.decodeFromHexString(referenceHexString)) + (cborEncodingAllVerifyingObjectTags to referenceHexString).let { (cbor, hex) -> + val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + } assertFailsWith(CborDecodingException::class) { Cbor { @@ -483,6 +550,7 @@ class CborTaggedTest { DataWithTags.serializer(), noTags ) + //the struct stuff is already tested before } } @@ -520,6 +588,21 @@ class CborTaggedTest { ) }.message ?: "", "CBOR tags [55] do not match expected tags [56]" ) + + assertContains( + assertFailsWith( + CborDecodingException::class, + message = "CBOR tags [55] do not match declared tags [56]" + ) { + cbor.decodeFromCbor( + DataWithTags.serializer(), cbor.decodeFromHexString( + CborElement.serializer(), + wrongTag55ForPropertyC + ) + ) + }.message ?: "", "CBOR tags [55] do not match expected tags [56]" + ) + } listOf( Cbor { @@ -540,6 +623,7 @@ class CborTaggedTest { verifyKeyTags = false }).forEach { cbor -> assertEquals(reference, cbor.decodeFromHexString(wrongTag55ForPropertyC)) + assertEquals(reference, cbor.decodeFromCbor(cbor.decodeFromHexString(wrongTag55ForPropertyC))) } } @@ -571,30 +655,65 @@ class CborTaggedTest { assertEquals(referenceHexString, cbor.encodeToHexString(ClassAsTagged.serializer(), reference)) assertEquals(reference, cbor.decodeFromHexString(ClassAsTagged.serializer(), referenceHexString)) - val struct = Cbor.CoseCompliant.encodeToCbor(reference) - assertEquals(Cbor.decodeFromByteArray(referenceHexString.hexToByteArray()), struct) + (cbor to referenceHexString).let { (cbor, hexString) -> + val struct = cbor.encodeToCbor(reference) + assertEquals(hexString, cbor.encodeToHexString(CborElement.serializer(), struct)) + assertEquals(cbor.decodeFromHexString(hexString), struct) + assertEquals(reference, cbor.decodeFromCbor(ClassAsTagged.serializer(), struct)) + } + + val cborNoVerifyObjTags = Cbor { verifyObjectTags = false } assertEquals( reference, - Cbor { verifyObjectTags = false }.decodeFromHexString(ClassAsTagged.serializer(), referenceHexString) + cborNoVerifyObjTags.decodeFromHexString(ClassAsTagged.serializer(), referenceHexString) ) + (cborNoVerifyObjTags to referenceHexString).let { (cbor, hexString) -> + val struct = cbor.encodeToCbor(reference) + // NEQ: the ref string has object tags, but here we don't encode them + assertNotEquals(hexString, cbor.encodeToHexString(CborElement.serializer(), struct)) + // NEW, the hex string has the tags, so they are decoded, but the struct, created without object tags does not + assertNotEquals(cbor.decodeFromHexString(hexString), struct) + assertEquals(reference, cbor.decodeFromCbor(ClassAsTagged.serializer(), struct)) + } + val cborNoEncodeObjTags = Cbor { encodeObjectTags = false } assertEquals( untaggedHexString, - Cbor { encodeObjectTags = false }.encodeToHexString(ClassAsTagged.serializer(), reference) + cborNoEncodeObjTags.encodeToHexString(ClassAsTagged.serializer(), reference) ) + (cborNoEncodeObjTags to untaggedHexString).let { (cbor, hexString) -> + val struct = cbor.encodeToCbor(reference) + assertEquals(hexString, cbor.encodeToHexString(CborElement.serializer(), struct)) + assertEquals(cbor.decodeFromHexString(hexString), struct) + assertEquals(reference, cbor.decodeFromCbor(ClassAsTagged.serializer(), struct)) + } assertEquals( reference, - Cbor { verifyObjectTags = false }.decodeFromHexString(ClassAsTagged.serializer(), untaggedHexString) + cborNoVerifyObjTags.decodeFromHexString(ClassAsTagged.serializer(), untaggedHexString) ) + (cborNoVerifyObjTags to untaggedHexString).let { (cbor, hexString) -> + val struct = cbor.encodeToCbor(reference) + assertEquals(hexString, cbor.encodeToHexString(CborElement.serializer(), struct)) + assertEquals(cbor.decodeFromHexString(hexString), struct) + assertEquals(reference, cbor.decodeFromCbor(ClassAsTagged.serializer(), struct)) + } assertContains( assertFailsWith(CborDecodingException::class) { cbor.decodeFromHexString(ClassAsTagged.serializer(), untaggedHexString) }.message ?: "", "do not match expected tags" ) + assertContains( + assertFailsWith(CborDecodingException::class) { + cbor.decodeFromCbor( + ClassAsTagged.serializer(), + cbor.decodeFromHexString(CborElement.serializer(), untaggedHexString) + ) + }.message ?: "", "do not match expected tags" + ) /** * 81 # array(1) @@ -606,6 +725,10 @@ class CborTaggedTest { */ val listOfObjectTagged = listOf(reference) assertEquals("81d90539a163616c6713", Cbor.CoseCompliant.encodeToHexString(listOfObjectTagged)) + assertEquals( + "81d90539a163616c6713", + Cbor.CoseCompliant.encodeToHexString(Cbor.CoseCompliant.encodeToCbor(listOfObjectTagged)) + ) } @@ -677,6 +800,12 @@ class CborTaggedTest { } assertEquals(referenceHexString, cbor.encodeToHexString(NestedTagged.serializer(), reference)) assertEquals(reference, cbor.decodeFromHexString(NestedTagged.serializer(), referenceHexString)) + (cbor to referenceHexString).let { (cbor, hex) -> + val struct = cbor.encodeToCbor(reference) + assertEquals(struct, cbor.decodeFromHexString(hex)) + assertEquals(reference, cbor.decodeFromCbor(struct)) + } + assertEquals( "More tags found than the 1 tags specified", @@ -684,6 +813,18 @@ class CborTaggedTest { cbor.decodeFromHexString(NestedTagged.serializer(), referenceHexStringWithBogusTag) }.message ) + assertEquals( + "CBOR tags [19, 20] do not match expected tags [19]", + assertFailsWith( + CborDecodingException::class, + message = "CBOR tags [19, 20] do not match expected tags [19]" + ) { + cbor.decodeFromCbor( + NestedTagged.serializer(), + cbor.decodeFromHexString(CborElement.serializer(), referenceHexStringWithBogusTag) + ) + }.message + ) assertEquals( "CBOR tags null do not match expected tags [19]", @@ -691,56 +832,75 @@ class CborTaggedTest { cbor.decodeFromHexString(NestedTagged.serializer(), referenceHexStringWithMissingTag) }.message ) + assertEquals( + "CBOR tags null do not match expected tags [19]", + assertFailsWith(CborDecodingException::class, message = "CBOR tags null do not match expected tags [19]") { + cbor.decodeFromCbor( + NestedTagged.serializer(), + cbor.decodeFromHexString(CborElement.serializer(), referenceHexStringWithMissingTag) + ) + }.message + ) + val cborNoVerifying = Cbor { + encodeKeyTags = true + encodeValueTags = true + encodeObjectTags = true + verifyKeyTags = true + verifyValueTags = true + verifyObjectTags = false + } assertEquals( reference, - Cbor { - encodeKeyTags = true - encodeValueTags = true - encodeObjectTags = true - verifyKeyTags = true - verifyValueTags = true - verifyObjectTags = false - }.decodeFromHexString(NestedTagged.serializer(), referenceHexString) + cborNoVerifying.decodeFromHexString(NestedTagged.serializer(), referenceHexString) ) + (cborNoVerifying to referenceHexString).let { (cbor, hex) -> + val struct = cbor.encodeToCbor(reference) + assertEquals(struct, cbor.decodeFromHexString(hex)) + assertEquals(reference, cbor.decodeFromCbor(struct)) + } assertEquals( reference, - Cbor { - encodeKeyTags = true - encodeValueTags = true - encodeObjectTags = true - verifyKeyTags = true - verifyValueTags = true - verifyObjectTags = false - }.decodeFromHexString(NestedTagged.serializer(), superfluousTagged) + cborNoVerifying.decodeFromHexString(NestedTagged.serializer(), superfluousTagged) ) + (cborNoVerifying to superfluousTagged).let { (cbor, hex) -> + val struct = cbor.encodeToCbor(reference) + //there are more tags in the string + assertNotEquals(struct, cbor.decodeFromHexString(hex)) + assertEquals(reference, cbor.decodeFromCbor(struct)) + } + val cborNoEncode = Cbor { + encodeKeyTags = true + encodeValueTags = true + verifyKeyTags = true + verifyValueTags = true + verifyObjectTags = false + encodeObjectTags = false + } assertEquals( untaggedHexString, - Cbor { - encodeKeyTags = true - encodeValueTags = true - verifyKeyTags = true - verifyValueTags = true - verifyObjectTags = true - encodeObjectTags = false - }.encodeToHexString(NestedTagged.serializer(), reference) + cborNoEncode.encodeToHexString(NestedTagged.serializer(), reference) ) + (cborNoEncode to untaggedHexString).let { (cbor, hex) -> + val struct = cbor.encodeToCbor(reference) + assertEquals(struct, cbor.decodeFromHexString(hex)) + assertEquals(reference, cbor.decodeFromCbor(struct)) + } assertEquals( reference, - Cbor { - encodeKeyTags = true - encodeValueTags = true - encodeObjectTags = true - verifyKeyTags = true - verifyValueTags = true - verifyObjectTags = false - }.decodeFromHexString(NestedTagged.serializer(), untaggedHexString) + cborNoVerifying.decodeFromHexString(NestedTagged.serializer(), untaggedHexString) ) + (cborNoVerifying to untaggedHexString).let { (cbor, hex) -> + val struct = cbor.encodeToCbor(reference) + //NEQ: decoding from an untagged string means no tags coming in while encoding path above creates those tags + assertNotEquals(struct, cbor.decodeFromHexString(hex)) + assertEquals(reference, cbor.decodeFromCbor(struct)) + } assertContains( assertFailsWith(CborDecodingException::class) { @@ -750,21 +910,21 @@ class CborTaggedTest { assertContains( assertFailsWith(CborDecodingException::class) { - Cbor { - encodeKeyTags = true - encodeValueTags = true - encodeObjectTags = true - verifyKeyTags = true - verifyValueTags = true - verifyObjectTags = false - }.decodeFromHexString( + cbor.decodeFromCbor( + NestedTagged.serializer(), + cbor.decodeFromHexString(CborElement.serializer(), untaggedHexString) + ) + }.message ?: "", "do not match expected tags" + ) + + assertContains( + assertFailsWith(CborDecodingException::class) { + cborNoVerifying.decodeFromHexString( NestedTagged.serializer(), superfluousWrongTaggedTagged ) }.message ?: "", "do not start with specified tags" ) - - } // See https://www.rfc-editor.org/rfc/rfc8949.html#name-self-described-cbor From d0305187a3fcae3a9c75bf7aead9705f01a72134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Fri, 8 Aug 2025 16:45:06 +0200 Subject: [PATCH 23/68] finalize api --- .../kotlinx/benchmarks/cbor/CborBaseLine.kt | 6 +- .../src/kotlinx/serialization/cbor/Cbor.kt | 37 ++++- .../kotlinx/serialization/cbor/CborElement.kt | 21 ++- .../serialization/cbor/CborArrayTest.kt | 21 ++- .../serialization/cbor/CborDecoderTest.kt | 38 ++--- .../cbor/CborDefiniteLengthTest.kt | 2 +- .../serialization/cbor/CborElementTest.kt | 38 ++--- .../kotlinx/serialization/cbor/CborIsoTest.kt | 4 +- .../serialization/cbor/CborLabelTest.kt | 22 +-- .../cbor/CborPolymorphismTest.kt | 12 +- .../serialization/cbor/CborTaggedTest.kt | 134 +++++++++--------- .../serialization/cbor/CborWriterTest.kt | 7 +- 12 files changed, 190 insertions(+), 152 deletions(-) diff --git a/benchmark/src/jmh/kotlin/kotlinx/benchmarks/cbor/CborBaseLine.kt b/benchmark/src/jmh/kotlin/kotlinx/benchmarks/cbor/CborBaseLine.kt index fe7222e8e4..0b05d8f50e 100644 --- a/benchmark/src/jmh/kotlin/kotlinx/benchmarks/cbor/CborBaseLine.kt +++ b/benchmark/src/jmh/kotlin/kotlinx/benchmarks/cbor/CborBaseLine.kt @@ -52,7 +52,7 @@ open class CborBaseline { } val baseBytes = cbor.encodeToByteArray(KTestOuterMessage.serializer(), baseMessage) - val baseStruct = cbor.encodeToCbor(KTestOuterMessage.serializer(), baseMessage) + val baseStruct = cbor.encodeToCborElement(KTestOuterMessage.serializer(), baseMessage) @Benchmark fun toBytes() = cbor.encodeToByteArray(KTestOuterMessage.serializer(), baseMessage) @@ -68,9 +68,9 @@ open class CborBaseline { fun structFromBytes() = cbor.decodeFromByteArray(CborElement.serializer(), baseBytes) @Benchmark - fun fromStruct() = cbor.decodeFromCbor(KTestOuterMessage.serializer(), baseStruct) + fun fromStruct() = cbor.decodeFromCborElement(KTestOuterMessage.serializer(), baseStruct) @Benchmark - fun toStruct() = cbor.encodeToCbor(KTestOuterMessage.serializer(), baseMessage) + fun toStruct() = cbor.encodeToCborElement(KTestOuterMessage.serializer(), baseMessage) } diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt index 1c5354ccae..2062dcc41c 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt @@ -95,25 +95,48 @@ public sealed class Cbor( return result } - public fun decodeFromCbor(deserializer: DeserializationStrategy, element: CborElement): T { + /** + * Deserializes the given [element] into a value of type [T] using the given [deserializer]. + * + * @throws [SerializationException] if the given CBOR element is not a valid CBOR input for the type [T] + * @throws [IllegalArgumentException] if the decoded input cannot be represented as a valid instance of type [T] + */ + public fun decodeFromCborElement(deserializer: DeserializationStrategy, element: CborElement): T { val reader = CborReader(this, StructuredCborParser(element, configuration.verifyObjectTags)) return reader.decodeSerializableValue(deserializer) } - public fun encodeToCbor(serializer: SerializationStrategy, value: T): CborElement { + /** + * Serializes the given [value] into an equivalent [CborElement] using the given [serializer] + * + * @throws [SerializationException] if the given value cannot be serialized to CBOR + */ + public fun encodeToCborElement(serializer: SerializationStrategy, value: T): CborElement { val writer = StructuredCborWriter(this) writer.encodeSerializableValue(serializer, value) return writer.finalize() } } - +/** + * Serializes the given [value] into an equivalent [CborElement] using a serializer retrieved + * from reified type parameter. + * + * @throws [SerializationException] if the given value cannot be serialized to CBOR. + */ @ExperimentalSerializationApi -public inline fun Cbor.encodeToCbor(value: T): CborElement = - encodeToCbor(serializersModule.serializer(), value) +public inline fun Cbor.encodeToCborElement(value: T): CborElement = + encodeToCborElement(serializersModule.serializer(), value) +/** + * Deserializes the given [element] element into a value of type [T] using a deserializer retrieved + * from reified type parameter. + * + * @throws [SerializationException] if the given JSON element is not a valid CBOR input for the type [T] + * @throws [IllegalArgumentException] if the decoded input cannot be represented as a valid instance of type [T] + */ @ExperimentalSerializationApi -public inline fun Cbor.decodeFromCbor(element: CborElement): T = - decodeFromCbor(serializersModule.serializer(), element) +public inline fun Cbor.decodeFromCborElement(element: CborElement): T = + decodeFromCborElement(serializersModule.serializer(), element) @OptIn(ExperimentalSerializationApi::class) private class CborImpl( diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt index 42c6f0bcd9..b33a18ac9f 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -16,7 +16,7 @@ import kotlinx.serialization.cbor.internal.* * * [CborElement.toString] properly prints CBOR tree as a human-readable representation. * Whole hierarchy is serializable, but only when used with [Cbor] as [CborElement] is purely CBOR-specific structure - * which has a meaningful schemaless semantics only for CBOR. + * which has meaningful schemaless semantics only for CBOR. * * The whole hierarchy is [serializable][Serializable] only by [Cbor] format. */ @@ -38,7 +38,7 @@ public sealed class CborElement( */ @OptIn(ExperimentalUnsignedTypes::class) public var tags: ULongArray = tags - internal set + internal set //need this to collect override fun equals(other: Any?): Boolean { if (this === other) return true @@ -89,18 +89,35 @@ public sealed class CborPrimitive( } } +/** + * Class representing either: + * * signed CBOR integer (major type 1) + * * unsigned CBOR integer (major type 0) + * + * depending on whether a positive or a negative number was passed. + */ @Serializable(with = CborIntSerializer::class) public sealed class CborInt( tags: ULongArray = ulongArrayOf(), value: T, ) : CborPrimitive(value, tags) { public companion object { + /** + * Creates: + * * signed CBOR integer (major type 1) + * * unsigned CBOR integer (major type 0) + * + * depending on whether a positive or a negative number was passed. + */ public operator fun invoke( value: Long, vararg tags: ULong ): CborInt<*> = if (value >= 0) CborPositiveInt(value.toULong(), tags = tags) else CborNegativeInt(value, tags = tags) + /** + * Creates an unsigned CBOR integer (major type 0). + */ public operator fun invoke( value: ULong, vararg tags: ULong diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborArrayTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborArrayTest.kt index c20bbcffdc..effe128e51 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborArrayTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborArrayTest.kt @@ -1,7 +1,6 @@ package kotlinx.serialization.cbor import kotlinx.serialization.* -import kotlinx.serialization.cbor.CborIsoTest.DataClass import kotlin.test.* @@ -20,8 +19,8 @@ class CborArrayTest { assertEquals(referenceHexString, cbor.encodeToHexString(ClassAs1Array.serializer(), reference)) assertEquals(reference, cbor.decodeFromHexString(ClassAs1Array.serializer(), referenceHexString)) - val struct = cbor.encodeToCbor(ClassAs1Array.serializer(), reference) - assertEquals(reference, cbor.decodeFromCbor(ClassAs1Array.serializer(), struct)) + val struct = cbor.encodeToCborElement(ClassAs1Array.serializer(), reference) + assertEquals(reference, cbor.decodeFromCborElement(ClassAs1Array.serializer(), struct)) assertEquals(referenceHexString, cbor.encodeToHexString(CborElement.serializer(), struct)) } @@ -41,8 +40,8 @@ class CborArrayTest { assertEquals(referenceHexString, cbor.encodeToHexString(ClassAs2Array.serializer(), reference)) assertEquals(reference, cbor.decodeFromHexString(ClassAs2Array.serializer(), referenceHexString)) - val struct = cbor.encodeToCbor(ClassAs2Array.serializer(), reference) - assertEquals(reference, cbor.decodeFromCbor(ClassAs2Array.serializer(), struct)) + val struct = cbor.encodeToCborElement(ClassAs2Array.serializer(), reference) + assertEquals(reference, cbor.decodeFromCborElement(ClassAs2Array.serializer(), struct)) assertEquals(referenceHexString, cbor.encodeToHexString(CborElement.serializer(), struct)) } @@ -64,8 +63,8 @@ class CborArrayTest { assertEquals(referenceHexString, cbor.encodeToHexString(ClassAs4ArrayNullable.serializer(), reference)) assertEquals(reference, cbor.decodeFromHexString(ClassAs4ArrayNullable.serializer(), referenceHexString)) - val struct = cbor.encodeToCbor(ClassAs4ArrayNullable.serializer(), reference) - assertEquals(reference, cbor.decodeFromCbor(ClassAs4ArrayNullable.serializer(), struct)) + val struct = cbor.encodeToCborElement(ClassAs4ArrayNullable.serializer(), reference) + assertEquals(reference, cbor.decodeFromCborElement(ClassAs4ArrayNullable.serializer(), struct)) assertEquals(referenceHexString, cbor.encodeToHexString(CborElement.serializer(), struct)) } @@ -89,8 +88,8 @@ class CborArrayTest { assertEquals(reference, cbor.decodeFromHexString(ClassWithArray.serializer(), referenceHexString)) - val struct = cbor.encodeToCbor(ClassWithArray.serializer(), reference) - assertEquals(reference, cbor.decodeFromCbor(ClassWithArray.serializer(), struct)) + val struct = cbor.encodeToCborElement(ClassWithArray.serializer(), reference) + assertEquals(reference, cbor.decodeFromCborElement(ClassWithArray.serializer(), struct)) assertEquals(referenceHexString, cbor.encodeToHexString(CborElement.serializer(), struct)) } @@ -115,10 +114,10 @@ class CborArrayTest { assertEquals(referenceHexString, cbor.encodeToHexString(DoubleTaggedClassWithArray.serializer(), reference)) assertEquals(reference, cbor.decodeFromHexString(DoubleTaggedClassWithArray.serializer(), referenceHexString)) - val struct = cbor.encodeToCbor(DoubleTaggedClassWithArray.serializer(), reference) + val struct = cbor.encodeToCborElement(DoubleTaggedClassWithArray.serializer(), reference) val structFromHex = cbor.decodeFromHexString(CborElement.serializer(), referenceHexString) assertEquals(structFromHex, struct) - assertEquals(reference, cbor.decodeFromCbor(DoubleTaggedClassWithArray.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(DoubleTaggedClassWithArray.serializer(), struct)) assertEquals(referenceHexString, cbor.encodeToHexString(CborElement.serializer(), struct)) } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt index 73b282436c..e51c2c9b10 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt @@ -22,7 +22,7 @@ class CborDecoderTest { assertEquals(reference, Cbor.decodeFromHexString(Simple.serializer(), hex)) val struct = Cbor.decodeFromHexString(hex) - assertEquals(reference, Cbor.decodeFromCbor(Simple.serializer(), struct)) + assertEquals(reference, Cbor.decodeFromCborElement(Simple.serializer(), struct)) assertEquals(hex, Cbor.encodeToHexString(CborElement.serializer(), struct)) } @@ -52,8 +52,8 @@ class CborDecoderTest { ) val struct = Cbor.decodeFromHexString(hex) - assertEquals(Cbor.encodeToCbor(test), struct) - assertEquals(test, Cbor.decodeFromCbor(TypesUmbrella.serializer(), struct)) + assertEquals(Cbor.encodeToCborElement(test), struct) + assertEquals(test, Cbor.decodeFromCborElement(TypesUmbrella.serializer(), struct)) assertEquals(hex, Cbor.encodeToHexString(TypesUmbrella.serializer(), test)) assertEquals(hex, Cbor.encodeToHexString(CborElement.serializer(), struct)) @@ -71,8 +71,8 @@ class CborDecoderTest { ) val structDef = Cbor.decodeFromHexString(hexDef) - assertEquals(Cbor.encodeToCbor(test), structDef) - assertEquals(test, Cbor.decodeFromCbor(TypesUmbrella.serializer(), structDef)) + assertEquals(Cbor.encodeToCborElement(test), structDef) + assertEquals(test, Cbor.decodeFromCborElement(TypesUmbrella.serializer(), structDef)) } @@ -95,7 +95,7 @@ class CborDecoderTest { ) val struct = Cbor.decodeFromHexString(hex) - assertEquals(expected, Cbor.decodeFromCbor(NullableByteString.serializer(), struct)) + assertEquals(expected, Cbor.decodeFromCborElement(NullableByteString.serializer(), struct)) /* A1 # map(1) * 6A # text(10) @@ -113,14 +113,14 @@ class CborDecoderTest { ) val structNull = Cbor.decodeFromHexString(hexNull) - assertEquals(expectedNull, Cbor.decodeFromCbor(NullableByteString.serializer(), structNull)) + assertEquals(expectedNull, Cbor.decodeFromCborElement(NullableByteString.serializer(), structNull)) } @Test fun testNullables() { assertEquals(NullableByteStringDefaultNull(), Cbor.decodeFromHexString("a0")) val struct = Cbor.decodeFromHexString("a0") - assertEquals(NullableByteStringDefaultNull(), Cbor.decodeFromCbor(NullableByteStringDefaultNull.serializer(), struct)) + assertEquals(NullableByteStringDefaultNull(), Cbor.decodeFromCborElement(NullableByteStringDefaultNull.serializer(), struct)) } /** @@ -142,7 +142,7 @@ class CborDecoderTest { val struct = Cbor.decodeFromHexString(hex) assertFailsWithMessage("Field 'a' is required") { - ignoreUnknownKeys.decodeFromCbor( + ignoreUnknownKeys.decodeFromCborElement( Simple.serializer(), struct ) @@ -160,7 +160,7 @@ class CborDecoderTest { val structDef = Cbor.decodeFromHexString(hexDef) assertFailsWithMessage("Field 'a' is required") { - ignoreUnknownKeys.decodeFromCbor( + ignoreUnknownKeys.decodeFromCborElement( Simple.serializer(), structDef ) @@ -269,7 +269,7 @@ class CborDecoderTest { ) ) val struct = Cbor.decodeFromHexString(hex) - assertEquals(expected, ignoreUnknownKeys.decodeFromCbor(Simple.serializer(), struct)) + assertEquals(expected, ignoreUnknownKeys.decodeFromCborElement(Simple.serializer(), struct)) } @@ -313,7 +313,7 @@ class CborDecoderTest { ) val struct = Cbor.decodeFromHexString(hex) - assertEquals(expected, ignoreUnknownKeys.decodeFromCbor(Simple.serializer(), struct)) + assertEquals(expected, ignoreUnknownKeys.decodeFromCborElement(Simple.serializer(), struct)) } /** @@ -410,7 +410,7 @@ class CborDecoderTest { ) ) val struct = Cbor.decodeFromHexString(hex) - assertEquals(expected, ignoreUnknownKeys.decodeFromCbor(SealedBox.serializer(), struct)) + assertEquals(expected, ignoreUnknownKeys.decodeFromCborElement(SealedBox.serializer(), struct)) } @@ -423,7 +423,7 @@ class CborDecoderTest { actual = Cbor.decodeFromHexString(hex) ) val struct = Cbor.decodeFromHexString(hex) - assertEquals(expected, Cbor.decodeFromCbor(TypeWithCustomByteString.serializer(), struct)) + assertEquals(expected, Cbor.decodeFromCborElement(TypeWithCustomByteString.serializer(), struct)) } @@ -436,7 +436,7 @@ class CborDecoderTest { actual = Cbor.decodeFromHexString(hex) ) val struct = Cbor.decodeFromHexString(hex) - assertEquals(expected, Cbor.decodeFromCbor(TypeWithNullableCustomByteString.serializer(), struct)) + assertEquals(expected, Cbor.decodeFromCborElement(TypeWithNullableCustomByteString.serializer(), struct)) } @@ -449,7 +449,7 @@ class CborDecoderTest { actual = Cbor.decodeFromHexString(hex) ) val struct = Cbor.decodeFromHexString(hex) - assertEquals(expected, Cbor.decodeFromCbor(TypeWithNullableCustomByteString.serializer(), struct)) + assertEquals(expected, Cbor.decodeFromCborElement(TypeWithNullableCustomByteString.serializer(), struct)) } @@ -462,7 +462,7 @@ class CborDecoderTest { actual = Cbor.decodeFromHexString(hex).x ) val struct = Cbor.decodeFromHexString(hex) - assertContentEquals(expected, Cbor.decodeFromCbor(ValueClassWithByteString.serializer(), struct).x) + assertContentEquals(expected, Cbor.decodeFromCborElement(ValueClassWithByteString.serializer(), struct).x) } @@ -475,7 +475,7 @@ class CborDecoderTest { actual = Cbor.decodeFromHexString(hex) ) val struct = Cbor.decodeFromHexString(hex) - assertEquals(expected, Cbor.decodeFromCbor(ValueClassWithCustomByteString.serializer(), struct)) + assertEquals(expected, Cbor.decodeFromCborElement(ValueClassWithCustomByteString.serializer(), struct)) } @@ -492,7 +492,7 @@ class CborDecoderTest { actual = Cbor.decodeFromHexString(hex).x.x ) val struct = Cbor.decodeFromHexString(hex) - assertContentEquals(expected, Cbor.decodeFromCbor(ValueClassWithUnlabeledByteString.serializer(), struct).x.x) + assertContentEquals(expected, Cbor.decodeFromCborElement(ValueClassWithUnlabeledByteString.serializer(), struct).x.x) } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDefiniteLengthTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDefiniteLengthTest.kt index d43704d05f..ab7425d73f 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDefiniteLengthTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDefiniteLengthTest.kt @@ -31,7 +31,7 @@ class CborDefiniteLengthTest { Cbor { useDefiniteLengthEncoding = true }.run { encodeToHexString( CborElement.serializer(), - encodeToCbor(TypesUmbrella.serializer(), test) + encodeToCborElement(TypesUmbrella.serializer(), test) ) } ) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index d887995622..ad060a5b81 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -344,11 +344,11 @@ class CborElementTest { "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472406b63626f72456c656d656e74f46f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564d90921181aff" ) .let { (cbor, obj, hex) -> - val struct = cbor.encodeToCbor(obj) + val struct = cbor.encodeToCborElement(obj) assertEquals(hex, cbor.encodeToHexString(obj)) assertEquals(hex, cbor.encodeToHexString(struct)) assertEquals(struct, cbor.decodeFromHexString(hex)) - assertEquals(obj, cbor.decodeFromCbor(struct)) + assertEquals(obj, cbor.decodeFromCborElement(struct)) assertEquals(obj, cbor.decodeFromHexString(hex)) } @@ -371,11 +371,11 @@ class CborElementTest { "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472f66b63626f72456c656d656e74f46f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564d90921181aff" ) .let { (cbor, obj, hex) -> - val struct = cbor.encodeToCbor(obj) + val struct = cbor.encodeToCborElement(obj) assertEquals(hex, cbor.encodeToHexString(obj)) assertEquals(hex, cbor.encodeToHexString(struct)) assertEquals(struct, cbor.decodeFromHexString(hex)) - assertEquals(obj, cbor.decodeFromCbor(struct)) + assertEquals(obj, cbor.decodeFromCborElement(struct)) assertEquals(obj, cbor.decodeFromHexString(hex)) } @@ -399,11 +399,11 @@ class CborElementTest { "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472f66b63626f72456c656d656e74bf4401030307f6ff6f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564d90921181aff" ) .let { (cbor, obj, hex) -> - val struct = cbor.encodeToCbor(obj) + val struct = cbor.encodeToCborElement(obj) assertEquals(hex, cbor.encodeToHexString(obj)) assertEquals(hex, cbor.encodeToHexString(struct)) assertEquals(struct, cbor.decodeFromHexString(hex)) - assertEquals(obj, cbor.decodeFromCbor(struct)) + assertEquals(obj, cbor.decodeFromCborElement(struct)) assertEquals(obj, cbor.decodeFromHexString(hex)) } @@ -428,7 +428,7 @@ class CborElementTest { "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472f66b63626f72456c656d656e74f66f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564d90921181aff" ) .let { (cbor, obj, hex) -> - val struct = cbor.encodeToCbor(obj) + val struct = cbor.encodeToCborElement(obj) assertEquals(hex, cbor.encodeToHexString(obj)) assertEquals(hex, cbor.encodeToHexString(struct)) assertEquals(struct, cbor.decodeFromHexString(hex)) @@ -456,11 +456,11 @@ class CborElementTest { "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472c1c3406b63626f72456c656d656e74f46f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564d90921181aff" ) .let { (cbor, obj, hex) -> - val struct = cbor.encodeToCbor(obj) + val struct = cbor.encodeToCborElement(obj) assertEquals(hex, cbor.encodeToHexString(obj)) assertEquals(hex, cbor.encodeToHexString(struct)) assertEquals(struct, cbor.decodeFromHexString(hex)) - assertEquals(obj, cbor.decodeFromCbor(struct)) + assertEquals(obj, cbor.decodeFromCborElement(struct)) assertEquals(obj, cbor.decodeFromHexString(hex)) } @@ -483,11 +483,11 @@ class CborElementTest { "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472c1c3406b63626f72456c656d656e74f46f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564181aff" ) .let { (cbor, obj, hex) -> - val struct = cbor.encodeToCbor(obj) + val struct = cbor.encodeToCborElement(obj) assertEquals(hex, cbor.encodeToHexString(obj)) assertEquals(hex, cbor.encodeToHexString(struct)) assertEquals(struct, cbor.decodeFromHexString(hex)) - assertEquals(obj, cbor.decodeFromCbor(struct)) + assertEquals(obj, cbor.decodeFromCborElement(struct)) assertEquals(obj, cbor.decodeFromHexString(hex)) } @@ -505,7 +505,7 @@ class CborElementTest { "bfd82a6b63626f72456c656d656e74d90921f4ff" ) .let { (cbor, obj, hex) -> - val struct = cbor.encodeToCbor(obj) + val struct = cbor.encodeToCborElement(obj) assertEquals(hex, cbor.encodeToHexString(obj)) assertEquals(hex, cbor.encodeToHexString(struct)) assertEquals(struct, cbor.decodeFromHexString(hex)) @@ -514,7 +514,7 @@ class CborElementTest { // hence, the following two will have: // Expected :MixedTag(cborElement=CborPrimitive(kind=Boolean, tags=, value=false)) // Actual :MixedTag(cborElement=CborPrimitive(kind=Boolean, tags=2337, value=false)) - assertNotEquals(obj, cbor.decodeFromCbor(struct)) + assertNotEquals(obj, cbor.decodeFromCborElement(struct)) assertNotEquals(obj, cbor.decodeFromHexString(hex)) } Triple( @@ -532,7 +532,7 @@ class CborElementTest { "bfd82a6b63626f72456c656d656e74d90921d85af4ff" ) .let { (cbor, obj, hex) -> - val struct = cbor.encodeToCbor(obj) + val struct = cbor.encodeToCborElement(obj) assertEquals(hex, cbor.encodeToHexString(obj)) assertEquals(hex, cbor.encodeToHexString(struct)) assertEquals(struct, cbor.decodeFromHexString(hex)) @@ -546,7 +546,7 @@ class CborElementTest { CborDecodingException::class, "CBOR tags [2337, 90] do not match expected tags [2337]" ) { - assertNotEquals(obj, cbor.decodeFromCbor(struct)) + assertNotEquals(obj, cbor.decodeFromCborElement(struct)) } assertFailsWith( CborDecodingException::class, @@ -571,7 +571,7 @@ class CborElementTest { "bfd82a6b63626f72456c656d656e74d90921d85af4ff" ) .let { (cbor, obj, hex) -> - val struct = cbor.encodeToCbor(obj) + val struct = cbor.encodeToCborElement(obj) assertEquals(hex, cbor.encodeToHexString(obj)) assertEquals(hex, cbor.encodeToHexString(struct)) assertEquals(struct, cbor.decodeFromHexString(hex)) @@ -580,7 +580,7 @@ class CborElementTest { // hence, the following two will have: // Expected :MixedTag(cborElement=CborPrimitive(kind=Boolean, tags=90, value=false)) // Actual :MixedTag(cborElement=CborPrimitive(kind=Boolean, tags=2337, 90, value=false)) - assertNotEquals(obj, cbor.decodeFromCbor(struct)) + assertNotEquals(obj, cbor.decodeFromCborElement(struct)) assertNotEquals(obj, cbor.decodeFromHexString(hex)) } @@ -599,12 +599,12 @@ class CborElementTest { "bfd82a6b63626f72456c656d656e74d85af4ff" ) .let { (cbor, obj, hex) -> - val struct = cbor.encodeToCbor(obj) + val struct = cbor.encodeToCborElement(obj) assertEquals(hex, cbor.encodeToHexString(obj)) assertEquals(hex, cbor.encodeToHexString(struct)) assertEquals(struct, cbor.decodeFromHexString(hex)) //no value tags means everything's fine again - assertEquals(obj, cbor.decodeFromCbor(struct)) + assertEquals(obj, cbor.decodeFromCborElement(struct)) assertEquals(obj, cbor.decodeFromHexString(hex)) } } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborIsoTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborIsoTest.kt index 03651091f1..1992cd0331 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborIsoTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborIsoTest.kt @@ -28,8 +28,8 @@ class CborIsoTest { assertEquals(reference, cbor.decodeFromHexString(DataClass.serializer(), referenceHexString)) assertEquals(referenceHexString, cbor.encodeToHexString(DataClass.serializer(), reference)) - val struct = cbor.encodeToCbor(DataClass.serializer(), reference) - assertEquals(reference, cbor.decodeFromCbor(DataClass.serializer(), struct)) + val struct = cbor.encodeToCborElement(DataClass.serializer(), reference) + assertEquals(reference, cbor.decodeFromCborElement(DataClass.serializer(), struct)) assertEquals(referenceHexString, cbor.encodeToHexString(CborElement.serializer(), struct)) } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborLabelTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborLabelTest.kt index 109376511c..06b82a9d63 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborLabelTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborLabelTest.kt @@ -36,8 +36,8 @@ class CborLabelTest { assertEquals(referenceHexLabelString, cbor.encodeToHexString(ClassWithCborLabel.serializer(), reference)) assertEquals(reference, cbor.decodeFromHexString(ClassWithCborLabel.serializer(), referenceHexLabelString)) - val struct = cbor.encodeToCbor(ClassWithCborLabel.serializer(), reference) - assertEquals(reference, cbor.decodeFromCbor(ClassWithCborLabel.serializer(), struct)) + val struct = cbor.encodeToCborElement(ClassWithCborLabel.serializer(), reference) + assertEquals(reference, cbor.decodeFromCborElement(ClassWithCborLabel.serializer(), struct)) assertEquals(referenceHexLabelString, cbor.encodeToHexString(CborElement.serializer(), struct)) } @@ -50,8 +50,8 @@ class CborLabelTest { assertEquals(reference, cbor.decodeFromHexString(ClassWithCborLabel.serializer(), referenceHexNameString)) - val struct = cbor.encodeToCbor(ClassWithCborLabel.serializer(), reference) - assertEquals(reference, cbor.decodeFromCbor(ClassWithCborLabel.serializer(), struct)) + val struct = cbor.encodeToCborElement(ClassWithCborLabel.serializer(), reference) + assertEquals(reference, cbor.decodeFromCborElement(ClassWithCborLabel.serializer(), struct)) assertEquals(referenceHexNameString, cbor.encodeToHexString(CborElement.serializer(), struct)) } @@ -74,8 +74,8 @@ class CborLabelTest { assertEquals(referenceHexLabelWithTagString, cbor.encodeToHexString(ClassWithCborLabelAndTag.serializer(), referenceWithTag)) assertEquals(referenceWithTag, cbor.decodeFromHexString(ClassWithCborLabelAndTag.serializer(), referenceHexLabelWithTagString)) - val struct = cbor.encodeToCbor(ClassWithCborLabelAndTag.serializer(), referenceWithTag) - assertEquals(referenceWithTag, cbor.decodeFromCbor(ClassWithCborLabelAndTag.serializer(), struct)) + val struct = cbor.encodeToCborElement(ClassWithCborLabelAndTag.serializer(), referenceWithTag) + assertEquals(referenceWithTag, cbor.decodeFromCborElement(ClassWithCborLabelAndTag.serializer(), struct)) assertEquals(referenceHexLabelWithTagString, cbor.encodeToHexString(CborElement.serializer(), struct)) } @@ -104,7 +104,7 @@ class CborLabelTest { //we cannot deserialize from the struct since it does not match the class structure assertFailsWith(CborDecodingException::class) { - cbor.decodeFromCbor(ClassWithCborLabelAndTag.serializer(), struct) + cbor.decodeFromCborElement(ClassWithCborLabelAndTag.serializer(), struct) } } @@ -131,7 +131,7 @@ class CborLabelTest { assertEquals(referenceWithTag, cbor.decodeFromHexString(ClassWithCborLabelAndTag.serializer(), referenceHexLabelWithTagString)) //no unknown props - val struct = cbor.encodeToCbor(ClassWithCborLabelAndTag.serializer(), referenceWithTag) + val struct = cbor.encodeToCborElement(ClassWithCborLabelAndTag.serializer(), referenceWithTag) //with unknown props val structFromString = cbor.decodeFromHexString(CborElement.serializer(), referenceHexLabelWithTagString) @@ -139,7 +139,7 @@ class CborLabelTest { assertNotEquals(struct, structFromString) assertNotEquals(referenceHexLabelWithTagString, cbor.encodeToHexString(CborElement.serializer(), struct)) - assertEquals(referenceWithTag, cbor.decodeFromCbor(ClassWithCborLabelAndTag.serializer(), struct)) + assertEquals(referenceWithTag, cbor.decodeFromCborElement(ClassWithCborLabelAndTag.serializer(), struct)) assertEquals(referenceHexLabelWithTagString, cbor.encodeToHexString(CborElement.serializer(), structFromString)) } @@ -163,8 +163,8 @@ class CborLabelTest { assertEquals(referenceWithoutLabel, cbor.decodeFromHexString(ClassWithoutCborLabel.serializer(), referenceHexStringWithoutLabel)) - val struct = cbor.encodeToCbor(ClassWithoutCborLabel.serializer(), referenceWithoutLabel) - assertEquals(referenceWithoutLabel, cbor.decodeFromCbor(ClassWithoutCborLabel.serializer(), struct)) + val struct = cbor.encodeToCborElement(ClassWithoutCborLabel.serializer(), referenceWithoutLabel) + assertEquals(referenceWithoutLabel, cbor.decodeFromCborElement(ClassWithoutCborLabel.serializer(), struct)) assertEquals(referenceHexStringWithoutLabel, cbor.encodeToHexString(CborElement.serializer(), struct)) } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborPolymorphismTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborPolymorphismTest.kt index bc033173eb..e3a72e27ce 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborPolymorphismTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborPolymorphismTest.kt @@ -28,10 +28,10 @@ class CborPolymorphismTest { hexResultToCheck = hexResultToCheck ) - val struct = cbor.encodeToCbor(A.serializer(), original) + val struct = cbor.encodeToCborElement(A.serializer(), original) assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hexResultToCheck)) assertEquals(hexResultToCheck, cbor.encodeToHexString(struct)) - assertEquals(original, cbor.decodeFromCbor(A.serializer(), struct)) + assertEquals(original, cbor.decodeFromCborElement(A.serializer(), struct)) } @Test @@ -44,8 +44,8 @@ class CborPolymorphismTest { ) assertSerializedToBinaryAndRestored(obj, SealedBox.serializer(), cbor) - val struct = cbor.encodeToCbor(SealedBox.serializer(), obj) - assertEquals(obj, cbor.decodeFromCbor(SealedBox.serializer(), struct)) + val struct = cbor.encodeToCborElement(SealedBox.serializer(), obj) + assertEquals(obj, cbor.decodeFromCborElement(SealedBox.serializer(), struct)) } @Test @@ -59,7 +59,7 @@ class CborPolymorphismTest { assertSerializedToBinaryAndRestored(obj, PolyBox.serializer(), cbor) - val struct = cbor.encodeToCbor(PolyBox.serializer(), obj) - assertEquals(obj, cbor.decodeFromCbor(PolyBox.serializer(), struct)) + val struct = cbor.encodeToCborElement(PolyBox.serializer(), obj) + assertEquals(obj, cbor.decodeFromCborElement(PolyBox.serializer(), struct)) } } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborTaggedTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborTaggedTest.kt index 298e38a039..68263fc304 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborTaggedTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborTaggedTest.kt @@ -234,9 +234,9 @@ class CborTaggedTest { } assertEquals(referenceHexString, cbor.encodeToHexString(DataWithTags.serializer(), reference)) val structFromHex = cbor.decodeFromHexString(CborElement.serializer(), referenceHexString) - val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + val struct = cbor.encodeToCborElement(DataWithTags.serializer(), reference) assertEquals(struct, structFromHex) - assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(DataWithTags.serializer(), struct)) assertEquals(referenceHexString, cbor.encodeToHexString(CborElement.serializer(), struct)) val cborDef = Cbor { @@ -250,17 +250,17 @@ class CborTaggedTest { } assertEquals(referenceHexStringDefLen, cborDef.encodeToHexString(DataWithTags.serializer(), reference)) val structDefFromHex = cborDef.decodeFromHexString(CborElement.serializer(), referenceHexStringDefLen) - val structDef = cborDef.encodeToCbor(DataWithTags.serializer(), reference) + val structDef = cborDef.encodeToCborElement(DataWithTags.serializer(), reference) assertEquals(structDef, structDefFromHex) - assertEquals(reference, cborDef.decodeFromCbor(DataWithTags.serializer(), structDef)) + assertEquals(reference, cborDef.decodeFromCborElement(DataWithTags.serializer(), structDef)) assertEquals(referenceHexStringDefLen, cborDef.encodeToHexString(CborElement.serializer(), structDef)) assertEquals(reference, Cbor.CoseCompliant.decodeFromHexString(DataWithTags.serializer(), referenceHexString)) val structCoseFromHex = Cbor.CoseCompliant.decodeFromHexString(CborElement.serializer(), referenceHexString) - val structCose = Cbor.CoseCompliant.encodeToCbor(DataWithTags.serializer(), reference) + val structCose = Cbor.CoseCompliant.encodeToCborElement(DataWithTags.serializer(), reference) assertEquals(structCose, structCoseFromHex) - assertEquals(reference, Cbor.CoseCompliant.decodeFromCbor(DataWithTags.serializer(), structCose)) + assertEquals(reference, Cbor.CoseCompliant.decodeFromCborElement(DataWithTags.serializer(), structCose)) assertEquals( reference, @@ -268,9 +268,9 @@ class CborTaggedTest { ) val structCoseFromHexDef = Cbor.CoseCompliant.decodeFromHexString(CborElement.serializer(), referenceHexStringDefLen) - val structCoseDef = Cbor.CoseCompliant.encodeToCbor(DataWithTags.serializer(), reference) + val structCoseDef = Cbor.CoseCompliant.encodeToCborElement(DataWithTags.serializer(), reference) assertEquals(structCoseDef, structCoseFromHexDef) - assertEquals(reference, Cbor.CoseCompliant.decodeFromCbor(DataWithTags.serializer(), structCoseDef)) + assertEquals(reference, Cbor.CoseCompliant.decodeFromCborElement(DataWithTags.serializer(), structCoseDef)) } @@ -286,9 +286,9 @@ class CborTaggedTest { } assertEquals(noKeyTags, cborNoKeyTags.encodeToHexString(DataWithTags.serializer(), reference)) (cborNoKeyTags to noKeyTags).let { (cbor, hex) -> - val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + val struct = cbor.encodeToCborElement(DataWithTags.serializer(), reference) assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) - assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(DataWithTags.serializer(), struct)) } val cborNoKeyTagsDefLen = Cbor { @@ -302,11 +302,11 @@ class CborTaggedTest { } assertEquals(noKeyTagsDefLen, cborNoKeyTagsDefLen.encodeToHexString(DataWithTags.serializer(), reference)) (cborNoKeyTagsDefLen to noKeyTagsDefLen).let { (cbor, hex) -> - val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + val struct = cbor.encodeToCborElement(DataWithTags.serializer(), reference) assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) // this must fail, because encoding/decoding is not symmetric with the current config (the struct does not have the tags, but the hex string does) assertFailsWith(CborDecodingException::class) { - assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(DataWithTags.serializer(), struct)) } } @@ -320,26 +320,26 @@ class CborTaggedTest { } assertEquals(reference, cborEncodingKeyTags.decodeFromHexString(noKeyTags)) (cborEncodingKeyTags to noKeyTags).let { (cbor, hex) -> - val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + val struct = cbor.encodeToCborElement(DataWithTags.serializer(), reference) // this must not be equal, because the scruct has the tags, but the hex string doesn't assertNotEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) - assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(DataWithTags.serializer(), struct)) } assertEquals(reference, cborEncodingKeyTags.decodeFromHexString(noKeyTagsDefLen)) (cborEncodingKeyTags to noKeyTagsDefLen).let { (cbor, hex) -> - val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + val struct = cbor.encodeToCborElement(DataWithTags.serializer(), reference) // this must not be equals, because the scruct has the tags, but the hex string doesn't (as above) assertNotEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) - assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(DataWithTags.serializer(), struct)) } assertEquals(reference, cborEncodingKeyTags.decodeFromHexString(referenceHexString)) (cborNoKeyTags to noKeyTags).let { (cbor, hex) -> - val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + val struct = cbor.encodeToCborElement(DataWithTags.serializer(), reference) assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) - assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(DataWithTags.serializer(), struct)) } assertFailsWith(CborDecodingException::class) { @@ -368,11 +368,11 @@ class CborTaggedTest { } assertEquals(noValueTags, cborNoValueTags.encodeToHexString(DataWithTags.serializer(), reference)) (cborNoValueTags to noValueTags).let { (cbor, hex) -> - val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + val struct = cbor.encodeToCborElement(DataWithTags.serializer(), reference) assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) // no value tags are written to the struct, so this will fail assertFailsWith(CborDecodingException::class) { - assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(DataWithTags.serializer(), struct)) } } @@ -387,18 +387,18 @@ class CborTaggedTest { } assertEquals(reference, cborEncodingValueTags.decodeFromHexString(noValueTags)) (cborEncodingValueTags to noValueTags).let { (cbor, hex) -> - val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + val struct = cbor.encodeToCborElement(DataWithTags.serializer(), reference) //hex is missing the tags, struct has them from the serializer assertNotEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) - assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(DataWithTags.serializer(), struct)) } assertEquals(reference, cborEncodingValueTags.decodeFromHexString(referenceHexString)) (cborEncodingValueTags to referenceHexString).let { (cbor, hex) -> - val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + val struct = cbor.encodeToCborElement(DataWithTags.serializer(), reference) assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) - assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(DataWithTags.serializer(), struct)) } assertFailsWith(CborDecodingException::class) { @@ -438,11 +438,11 @@ class CborTaggedTest { } assertEquals(noTags, cborNoTags.encodeToHexString(DataWithTags.serializer(), reference)) (cborNoTags to noTags).let { (cbor, hex) -> - val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + val struct = cbor.encodeToCborElement(DataWithTags.serializer(), reference) assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) //struct is missing the tags assertFailsWith(CborDecodingException::class) { - assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(DataWithTags.serializer(), struct)) } } @@ -457,11 +457,11 @@ class CborTaggedTest { } assertEquals(noTagsDefLen, cborNoTagsDefLen.encodeToHexString(DataWithTags.serializer(), reference)) (cborNoTagsDefLen to noTagsDefLen).let { (cbor, hex) -> - val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + val struct = cbor.encodeToCborElement(DataWithTags.serializer(), reference) assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) //struct is missing the tags assertFailsWith(CborDecodingException::class) { - assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(DataWithTags.serializer(), struct)) } } @@ -475,18 +475,18 @@ class CborTaggedTest { } assertEquals(reference, cborEncodingAllVerifyingObjectTags.decodeFromHexString(noTags)) (cborEncodingAllVerifyingObjectTags to noTags).let { (cbor, hex) -> - val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + val struct = cbor.encodeToCborElement(DataWithTags.serializer(), reference) //hex has not tags but the current config encodes them into the struct assertNotEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) - assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(DataWithTags.serializer(), struct)) } assertEquals(reference, cborEncodingAllVerifyingObjectTags.decodeFromHexString(noTagsDefLen)) (cborEncodingAllVerifyingObjectTags to noTagsDefLen).let { (cbor, hex) -> - val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + val struct = cbor.encodeToCborElement(DataWithTags.serializer(), reference) //hex has not tags but the current config encodes them into the struct assertNotEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) - assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(DataWithTags.serializer(), struct)) } @@ -501,41 +501,41 @@ class CborTaggedTest { } assertEquals(reference, cborEncodingAllDefLen.decodeFromHexString(noTags)) (cborEncodingAllDefLen to noTags).let { (cbor, hex) -> - val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + val struct = cbor.encodeToCborElement(DataWithTags.serializer(), reference) //hex has not tags but the current config encodes them into the struct assertNotEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) - assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(DataWithTags.serializer(), struct)) } assertEquals(reference, cborEncodingAllDefLen.decodeFromHexString(noTagsDefLen)) (cborEncodingAllDefLen to noTagsDefLen).let { (cbor, hex) -> - val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + val struct = cbor.encodeToCborElement(DataWithTags.serializer(), reference) //hex has not tags but the current config encodes them into the struct assertNotEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) - assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(DataWithTags.serializer(), struct)) } assertEquals(reference, cborEncodingAllVerifyingObjectTags.decodeFromHexString(noKeyTags)) (cborEncodingAllVerifyingObjectTags to noKeyTags).let { (cbor, hex) -> - val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + val struct = cbor.encodeToCborElement(DataWithTags.serializer(), reference) //hex has not tags but the current config encodes them into the struct assertNotEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) - assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(DataWithTags.serializer(), struct)) } assertEquals(reference, cborEncodingAllVerifyingObjectTags.decodeFromHexString(noValueTags)) (cborEncodingAllVerifyingObjectTags to noValueTags).let { (cbor, hex) -> - val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + val struct = cbor.encodeToCborElement(DataWithTags.serializer(), reference) //hex has not tags but the current config encodes them into the struct assertNotEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) - assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(DataWithTags.serializer(), struct)) } assertEquals(reference, cborEncodingAllVerifyingObjectTags.decodeFromHexString(referenceHexString)) (cborEncodingAllVerifyingObjectTags to referenceHexString).let { (cbor, hex) -> - val struct = cbor.encodeToCbor(DataWithTags.serializer(), reference) + val struct = cbor.encodeToCborElement(DataWithTags.serializer(), reference) assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) - assertEquals(reference, cbor.decodeFromCbor(DataWithTags.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(DataWithTags.serializer(), struct)) } assertFailsWith(CborDecodingException::class) { @@ -594,7 +594,7 @@ class CborTaggedTest { CborDecodingException::class, message = "CBOR tags [55] do not match declared tags [56]" ) { - cbor.decodeFromCbor( + cbor.decodeFromCborElement( DataWithTags.serializer(), cbor.decodeFromHexString( CborElement.serializer(), wrongTag55ForPropertyC @@ -623,7 +623,7 @@ class CborTaggedTest { verifyKeyTags = false }).forEach { cbor -> assertEquals(reference, cbor.decodeFromHexString(wrongTag55ForPropertyC)) - assertEquals(reference, cbor.decodeFromCbor(cbor.decodeFromHexString(wrongTag55ForPropertyC))) + assertEquals(reference, cbor.decodeFromCborElement(cbor.decodeFromHexString(wrongTag55ForPropertyC))) } } @@ -656,10 +656,10 @@ class CborTaggedTest { assertEquals(reference, cbor.decodeFromHexString(ClassAsTagged.serializer(), referenceHexString)) (cbor to referenceHexString).let { (cbor, hexString) -> - val struct = cbor.encodeToCbor(reference) + val struct = cbor.encodeToCborElement(reference) assertEquals(hexString, cbor.encodeToHexString(CborElement.serializer(), struct)) assertEquals(cbor.decodeFromHexString(hexString), struct) - assertEquals(reference, cbor.decodeFromCbor(ClassAsTagged.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(ClassAsTagged.serializer(), struct)) } @@ -669,12 +669,12 @@ class CborTaggedTest { cborNoVerifyObjTags.decodeFromHexString(ClassAsTagged.serializer(), referenceHexString) ) (cborNoVerifyObjTags to referenceHexString).let { (cbor, hexString) -> - val struct = cbor.encodeToCbor(reference) + val struct = cbor.encodeToCborElement(reference) // NEQ: the ref string has object tags, but here we don't encode them assertNotEquals(hexString, cbor.encodeToHexString(CborElement.serializer(), struct)) // NEW, the hex string has the tags, so they are decoded, but the struct, created without object tags does not assertNotEquals(cbor.decodeFromHexString(hexString), struct) - assertEquals(reference, cbor.decodeFromCbor(ClassAsTagged.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(ClassAsTagged.serializer(), struct)) } val cborNoEncodeObjTags = Cbor { encodeObjectTags = false } @@ -683,10 +683,10 @@ class CborTaggedTest { cborNoEncodeObjTags.encodeToHexString(ClassAsTagged.serializer(), reference) ) (cborNoEncodeObjTags to untaggedHexString).let { (cbor, hexString) -> - val struct = cbor.encodeToCbor(reference) + val struct = cbor.encodeToCborElement(reference) assertEquals(hexString, cbor.encodeToHexString(CborElement.serializer(), struct)) assertEquals(cbor.decodeFromHexString(hexString), struct) - assertEquals(reference, cbor.decodeFromCbor(ClassAsTagged.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(ClassAsTagged.serializer(), struct)) } @@ -695,10 +695,10 @@ class CborTaggedTest { cborNoVerifyObjTags.decodeFromHexString(ClassAsTagged.serializer(), untaggedHexString) ) (cborNoVerifyObjTags to untaggedHexString).let { (cbor, hexString) -> - val struct = cbor.encodeToCbor(reference) + val struct = cbor.encodeToCborElement(reference) assertEquals(hexString, cbor.encodeToHexString(CborElement.serializer(), struct)) assertEquals(cbor.decodeFromHexString(hexString), struct) - assertEquals(reference, cbor.decodeFromCbor(ClassAsTagged.serializer(), struct)) + assertEquals(reference, cbor.decodeFromCborElement(ClassAsTagged.serializer(), struct)) } assertContains( @@ -708,7 +708,7 @@ class CborTaggedTest { ) assertContains( assertFailsWith(CborDecodingException::class) { - cbor.decodeFromCbor( + cbor.decodeFromCborElement( ClassAsTagged.serializer(), cbor.decodeFromHexString(CborElement.serializer(), untaggedHexString) ) @@ -727,7 +727,7 @@ class CborTaggedTest { assertEquals("81d90539a163616c6713", Cbor.CoseCompliant.encodeToHexString(listOfObjectTagged)) assertEquals( "81d90539a163616c6713", - Cbor.CoseCompliant.encodeToHexString(Cbor.CoseCompliant.encodeToCbor(listOfObjectTagged)) + Cbor.CoseCompliant.encodeToHexString(Cbor.CoseCompliant.encodeToCborElement(listOfObjectTagged)) ) @@ -801,9 +801,9 @@ class CborTaggedTest { assertEquals(referenceHexString, cbor.encodeToHexString(NestedTagged.serializer(), reference)) assertEquals(reference, cbor.decodeFromHexString(NestedTagged.serializer(), referenceHexString)) (cbor to referenceHexString).let { (cbor, hex) -> - val struct = cbor.encodeToCbor(reference) + val struct = cbor.encodeToCborElement(reference) assertEquals(struct, cbor.decodeFromHexString(hex)) - assertEquals(reference, cbor.decodeFromCbor(struct)) + assertEquals(reference, cbor.decodeFromCborElement(struct)) } @@ -819,7 +819,7 @@ class CborTaggedTest { CborDecodingException::class, message = "CBOR tags [19, 20] do not match expected tags [19]" ) { - cbor.decodeFromCbor( + cbor.decodeFromCborElement( NestedTagged.serializer(), cbor.decodeFromHexString(CborElement.serializer(), referenceHexStringWithBogusTag) ) @@ -835,7 +835,7 @@ class CborTaggedTest { assertEquals( "CBOR tags null do not match expected tags [19]", assertFailsWith(CborDecodingException::class, message = "CBOR tags null do not match expected tags [19]") { - cbor.decodeFromCbor( + cbor.decodeFromCborElement( NestedTagged.serializer(), cbor.decodeFromHexString(CborElement.serializer(), referenceHexStringWithMissingTag) ) @@ -856,9 +856,9 @@ class CborTaggedTest { cborNoVerifying.decodeFromHexString(NestedTagged.serializer(), referenceHexString) ) (cborNoVerifying to referenceHexString).let { (cbor, hex) -> - val struct = cbor.encodeToCbor(reference) + val struct = cbor.encodeToCborElement(reference) assertEquals(struct, cbor.decodeFromHexString(hex)) - assertEquals(reference, cbor.decodeFromCbor(struct)) + assertEquals(reference, cbor.decodeFromCborElement(struct)) } assertEquals( @@ -866,10 +866,10 @@ class CborTaggedTest { cborNoVerifying.decodeFromHexString(NestedTagged.serializer(), superfluousTagged) ) (cborNoVerifying to superfluousTagged).let { (cbor, hex) -> - val struct = cbor.encodeToCbor(reference) + val struct = cbor.encodeToCborElement(reference) //there are more tags in the string assertNotEquals(struct, cbor.decodeFromHexString(hex)) - assertEquals(reference, cbor.decodeFromCbor(struct)) + assertEquals(reference, cbor.decodeFromCborElement(struct)) } val cborNoEncode = Cbor { @@ -885,9 +885,9 @@ class CborTaggedTest { cborNoEncode.encodeToHexString(NestedTagged.serializer(), reference) ) (cborNoEncode to untaggedHexString).let { (cbor, hex) -> - val struct = cbor.encodeToCbor(reference) + val struct = cbor.encodeToCborElement(reference) assertEquals(struct, cbor.decodeFromHexString(hex)) - assertEquals(reference, cbor.decodeFromCbor(struct)) + assertEquals(reference, cbor.decodeFromCborElement(struct)) } @@ -896,10 +896,10 @@ class CborTaggedTest { cborNoVerifying.decodeFromHexString(NestedTagged.serializer(), untaggedHexString) ) (cborNoVerifying to untaggedHexString).let { (cbor, hex) -> - val struct = cbor.encodeToCbor(reference) + val struct = cbor.encodeToCborElement(reference) //NEQ: decoding from an untagged string means no tags coming in while encoding path above creates those tags assertNotEquals(struct, cbor.decodeFromHexString(hex)) - assertEquals(reference, cbor.decodeFromCbor(struct)) + assertEquals(reference, cbor.decodeFromCborElement(struct)) } assertContains( @@ -910,7 +910,7 @@ class CborTaggedTest { assertContains( assertFailsWith(CborDecodingException::class) { - cbor.decodeFromCbor( + cbor.decodeFromCborElement( NestedTagged.serializer(), cbor.decodeFromHexString(CborElement.serializer(), untaggedHexString) ) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt index f37624a1ff..1f9718ea5a 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt @@ -5,7 +5,6 @@ package kotlinx.serialization.cbor import kotlinx.serialization.* -import kotlinx.serialization.cbor.CborSkipTagAndEmptyTest.DataClass import kotlin.test.* @@ -34,7 +33,7 @@ class CbrWriterTest { encoded, Cbor.encodeToHexString(TypesUmbrella.serializer(), test) ) - val struct = Cbor.encodeToCbor(test) + val struct = Cbor.encodeToCborElement(test) assertEquals(Cbor.decodeFromByteArray(encoded.hexToByteArray()), struct) } @@ -57,7 +56,7 @@ class CbrWriterTest { encoded, Cbor { useDefiniteLengthEncoding = true }.encodeToHexString(TypesUmbrella.serializer(), test) ) - val struct = Cbor.encodeToCbor(test) + val struct = Cbor.encodeToCborElement(test) assertEquals(Cbor.decodeFromByteArray(encoded.hexToByteArray()), struct) } @@ -77,7 +76,7 @@ class CbrWriterTest { encoded, Cbor.encodeToHexString(NumberTypesUmbrella.serializer(), test) ) - val struct = Cbor.encodeToCbor(test) + val struct = Cbor.encodeToCborElement(test) assertEquals(Cbor.decodeFromByteArray(encoded.hexToByteArray()), struct) } From 2ce3fc60ee5872c4ed9ab2e9562285f1022f1207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Fri, 8 Aug 2025 16:50:26 +0200 Subject: [PATCH 24/68] APIDUMP --- .../cbor/api/kotlinx-serialization-cbor.api | 204 ++++++++++++++++++ .../api/kotlinx-serialization-cbor.klib.api | 157 ++++++++++++++ 2 files changed, 361 insertions(+) diff --git a/formats/cbor/api/kotlinx-serialization-cbor.api b/formats/cbor/api/kotlinx-serialization-cbor.api index 6b580add00..131b13e1ba 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 decodeFromCborElement (Lkotlinx/serialization/DeserializationStrategy;Lkotlinx/serialization/cbor/CborElement;)Ljava/lang/Object; public fun encodeToByteArray (Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)[B + public final fun encodeToCborElement (Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)Lkotlinx/serialization/cbor/CborElement; public final fun getConfiguration ()Lkotlinx/serialization/cbor/CborConfiguration; public fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule; } @@ -25,6 +27,15 @@ public final synthetic class kotlinx/serialization/cbor/CborArray$Impl : kotlinx public fun ()V } +public final class kotlinx/serialization/cbor/CborBoolean : kotlinx/serialization/cbor/CborPrimitive { + public static final field Companion Lkotlinx/serialization/cbor/CborBoolean$Companion; + public synthetic fun (Z[JLkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class kotlinx/serialization/cbor/CborBoolean$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class kotlinx/serialization/cbor/CborBuilder { public final fun getAlwaysUseByteString ()Z public final fun getEncodeDefaults ()Z @@ -52,6 +63,18 @@ public final class kotlinx/serialization/cbor/CborBuilder { public final fun setVerifyValueTags (Z)V } +public final class kotlinx/serialization/cbor/CborByteString : kotlinx/serialization/cbor/CborPrimitive { + public static final field Companion Lkotlinx/serialization/cbor/CborByteString$Companion; + public synthetic fun ([B[JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class kotlinx/serialization/cbor/CborByteString$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class kotlinx/serialization/cbor/CborConfiguration { public final fun getAlwaysUseByteString ()Z public final fun getEncodeDefaults ()Z @@ -68,6 +91,7 @@ public final class kotlinx/serialization/cbor/CborConfiguration { } public abstract interface class kotlinx/serialization/cbor/CborDecoder : kotlinx/serialization/encoding/Decoder { + public abstract fun decodeCborElement ()Lkotlinx/serialization/cbor/CborElement; public abstract fun getCbor ()Lkotlinx/serialization/cbor/Cbor; } @@ -76,6 +100,28 @@ public final class kotlinx/serialization/cbor/CborDecoder$DefaultImpls { public static fun decodeSerializableValue (Lkotlinx/serialization/cbor/CborDecoder;Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object; } +public final class kotlinx/serialization/cbor/CborDouble : kotlinx/serialization/cbor/CborPrimitive { + public static final field Companion Lkotlinx/serialization/cbor/CborDouble$Companion; + public synthetic fun (D[JLkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class kotlinx/serialization/cbor/CborDouble$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract class kotlinx/serialization/cbor/CborElement { + public static final field Companion Lkotlinx/serialization/cbor/CborElement$Companion; + public synthetic fun ([JILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun ([JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getTags-Y2RjT0g ()[J + public fun hashCode ()I +} + +public final class kotlinx/serialization/cbor/CborElement$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public abstract interface class kotlinx/serialization/cbor/CborEncoder : kotlinx/serialization/encoding/Encoder { public abstract fun getCbor ()Lkotlinx/serialization/cbor/Cbor; } @@ -87,6 +133,18 @@ public final class kotlinx/serialization/cbor/CborEncoder$DefaultImpls { public static fun encodeSerializableValue (Lkotlinx/serialization/cbor/CborEncoder;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)V } +public abstract class kotlinx/serialization/cbor/CborInt : kotlinx/serialization/cbor/CborPrimitive { + public static final field Companion Lkotlinx/serialization/cbor/CborInt$Companion; + public synthetic fun ([JLjava/lang/Object;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun ([JLjava/lang/Object;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class kotlinx/serialization/cbor/CborInt$Companion { + public final fun invoke-SIFponk (J[J)Lkotlinx/serialization/cbor/CborInt; + public final fun invoke-ahITK_k (J[J)Lkotlinx/serialization/cbor/CborInt; + public final fun serializer (Lkotlinx/serialization/KSerializer;)Lkotlinx/serialization/KSerializer; +} + public final class kotlinx/serialization/cbor/CborKt { public static final fun Cbor (Lkotlinx/serialization/cbor/Cbor;Lkotlin/jvm/functions/Function1;)Lkotlinx/serialization/cbor/Cbor; public static synthetic fun Cbor$default (Lkotlinx/serialization/cbor/Cbor;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/serialization/cbor/Cbor; @@ -101,6 +159,152 @@ public final synthetic class kotlinx/serialization/cbor/CborLabel$Impl : kotlinx public final synthetic fun label ()J } +public final class kotlinx/serialization/cbor/CborList : kotlinx/serialization/cbor/CborElement, java/util/List, kotlin/jvm/internal/markers/KMappedMarker { + public static final field Companion Lkotlinx/serialization/cbor/CborList$Companion; + public synthetic fun (Ljava/util/List;[JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun add (ILjava/lang/Object;)V + public fun add (ILkotlinx/serialization/cbor/CborElement;)V + public synthetic fun add (Ljava/lang/Object;)Z + public fun add (Lkotlinx/serialization/cbor/CborElement;)Z + public fun addAll (ILjava/util/Collection;)Z + public fun addAll (Ljava/util/Collection;)Z + public fun clear ()V + public final fun contains (Ljava/lang/Object;)Z + public fun contains (Lkotlinx/serialization/cbor/CborElement;)Z + public fun containsAll (Ljava/util/Collection;)Z + public fun equals (Ljava/lang/Object;)Z + public synthetic fun get (I)Ljava/lang/Object; + public fun get (I)Lkotlinx/serialization/cbor/CborElement; + public fun getSize ()I + public fun hashCode ()I + public final fun indexOf (Ljava/lang/Object;)I + public fun indexOf (Lkotlinx/serialization/cbor/CborElement;)I + public fun isEmpty ()Z + public fun iterator ()Ljava/util/Iterator; + public final fun lastIndexOf (Ljava/lang/Object;)I + public fun lastIndexOf (Lkotlinx/serialization/cbor/CborElement;)I + public fun listIterator ()Ljava/util/ListIterator; + public fun listIterator (I)Ljava/util/ListIterator; + public synthetic fun remove (I)Ljava/lang/Object; + public fun remove (I)Lkotlinx/serialization/cbor/CborElement; + public fun remove (Ljava/lang/Object;)Z + public fun removeAll (Ljava/util/Collection;)Z + public fun replaceAll (Ljava/util/function/UnaryOperator;)V + public fun retainAll (Ljava/util/Collection;)Z + public synthetic fun set (ILjava/lang/Object;)Ljava/lang/Object; + public fun set (ILkotlinx/serialization/cbor/CborElement;)Lkotlinx/serialization/cbor/CborElement; + public final fun size ()I + public fun sort (Ljava/util/Comparator;)V + public fun subList (II)Ljava/util/List; + public fun toArray ()[Ljava/lang/Object; + public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; + public fun toString ()Ljava/lang/String; +} + +public final class kotlinx/serialization/cbor/CborList$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class kotlinx/serialization/cbor/CborMap : kotlinx/serialization/cbor/CborElement, java/util/Map, kotlin/jvm/internal/markers/KMappedMarker { + public static final field Companion Lkotlinx/serialization/cbor/CborMap$Companion; + public synthetic fun (Ljava/util/Map;[JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun clear ()V + public synthetic fun compute (Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object; + public fun compute (Lkotlinx/serialization/cbor/CborElement;Ljava/util/function/BiFunction;)Lkotlinx/serialization/cbor/CborElement; + public synthetic fun computeIfAbsent (Ljava/lang/Object;Ljava/util/function/Function;)Ljava/lang/Object; + public fun computeIfAbsent (Lkotlinx/serialization/cbor/CborElement;Ljava/util/function/Function;)Lkotlinx/serialization/cbor/CborElement; + public synthetic fun computeIfPresent (Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object; + public fun computeIfPresent (Lkotlinx/serialization/cbor/CborElement;Ljava/util/function/BiFunction;)Lkotlinx/serialization/cbor/CborElement; + public final fun containsKey (Ljava/lang/Object;)Z + public fun containsKey (Lkotlinx/serialization/cbor/CborElement;)Z + public final fun containsValue (Ljava/lang/Object;)Z + public fun containsValue (Lkotlinx/serialization/cbor/CborElement;)Z + public final fun entrySet ()Ljava/util/Set; + public fun equals (Ljava/lang/Object;)Z + public final synthetic fun get (Ljava/lang/Object;)Ljava/lang/Object; + public final fun get (Ljava/lang/Object;)Lkotlinx/serialization/cbor/CborElement; + public fun get (Lkotlinx/serialization/cbor/CborElement;)Lkotlinx/serialization/cbor/CborElement; + public fun getEntries ()Ljava/util/Set; + public fun getKeys ()Ljava/util/Set; + public fun getSize ()I + public fun getValues ()Ljava/util/Collection; + public fun hashCode ()I + public fun isEmpty ()Z + public final fun keySet ()Ljava/util/Set; + public synthetic fun merge (Ljava/lang/Object;Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object; + public fun merge (Lkotlinx/serialization/cbor/CborElement;Lkotlinx/serialization/cbor/CborElement;Ljava/util/function/BiFunction;)Lkotlinx/serialization/cbor/CborElement; + public synthetic fun put (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public fun put (Lkotlinx/serialization/cbor/CborElement;Lkotlinx/serialization/cbor/CborElement;)Lkotlinx/serialization/cbor/CborElement; + public fun putAll (Ljava/util/Map;)V + public synthetic fun putIfAbsent (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public fun putIfAbsent (Lkotlinx/serialization/cbor/CborElement;Lkotlinx/serialization/cbor/CborElement;)Lkotlinx/serialization/cbor/CborElement; + public synthetic fun remove (Ljava/lang/Object;)Ljava/lang/Object; + public fun remove (Ljava/lang/Object;)Lkotlinx/serialization/cbor/CborElement; + public fun remove (Ljava/lang/Object;Ljava/lang/Object;)Z + public synthetic fun replace (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public synthetic fun replace (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Z + public fun replace (Lkotlinx/serialization/cbor/CborElement;Lkotlinx/serialization/cbor/CborElement;)Lkotlinx/serialization/cbor/CborElement; + public fun replace (Lkotlinx/serialization/cbor/CborElement;Lkotlinx/serialization/cbor/CborElement;Lkotlinx/serialization/cbor/CborElement;)Z + public fun replaceAll (Ljava/util/function/BiFunction;)V + public final fun size ()I + public fun toString ()Ljava/lang/String; + public final fun values ()Ljava/util/Collection; +} + +public final class kotlinx/serialization/cbor/CborMap$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class kotlinx/serialization/cbor/CborNegativeInt : kotlinx/serialization/cbor/CborInt { + public static final field Companion Lkotlinx/serialization/cbor/CborNegativeInt$Companion; + public synthetic fun (J[JLkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class kotlinx/serialization/cbor/CborNegativeInt$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class kotlinx/serialization/cbor/CborNull : kotlinx/serialization/cbor/CborPrimitive { + public static final field Companion Lkotlinx/serialization/cbor/CborNull$Companion; + public synthetic fun ([JLkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class kotlinx/serialization/cbor/CborNull$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class kotlinx/serialization/cbor/CborPositiveInt : kotlinx/serialization/cbor/CborInt { + public static final field Companion Lkotlinx/serialization/cbor/CborPositiveInt$Companion; + public synthetic fun (J[JLkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class kotlinx/serialization/cbor/CborPositiveInt$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract class kotlinx/serialization/cbor/CborPrimitive : kotlinx/serialization/cbor/CborElement { + public static final field Companion Lkotlinx/serialization/cbor/CborPrimitive$Companion; + public synthetic fun (Ljava/lang/Object;[JILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/Object;[JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getValue ()Ljava/lang/Object; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class kotlinx/serialization/cbor/CborPrimitive$Companion { + public final fun serializer (Lkotlinx/serialization/KSerializer;)Lkotlinx/serialization/KSerializer; +} + +public final class kotlinx/serialization/cbor/CborString : kotlinx/serialization/cbor/CborPrimitive { + public static final field Companion Lkotlinx/serialization/cbor/CborString$Companion; + public synthetic fun (Ljava/lang/String;[JLkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class kotlinx/serialization/cbor/CborString$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class kotlinx/serialization/cbor/CborTag { public static final field BASE16 J public static final field BASE64 J diff --git a/formats/cbor/api/kotlinx-serialization-cbor.klib.api b/formats/cbor/api/kotlinx-serialization-cbor.klib.api index 346416c8fa..6d5bce0a15 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.klib.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.klib.api @@ -45,6 +45,8 @@ open annotation class kotlinx.serialization.cbor/ValueTags : kotlin/Annotation { 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] + + abstract fun decodeCborElement(): kotlinx.serialization.cbor/CborElement // kotlinx.serialization.cbor/CborDecoder.decodeCborElement|decodeCborElement(){}[0] } abstract interface kotlinx.serialization.cbor/CborEncoder : kotlinx.serialization.encoding/Encoder { // kotlinx.serialization.cbor/CborEncoder|null[0] @@ -52,6 +54,14 @@ 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/CborBoolean : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborBoolean|null[0] + constructor (kotlin/Boolean, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborBoolean.|(kotlin.Boolean;kotlin.ULongArray...){}[0] + + final object Companion { // kotlinx.serialization.cbor/CborBoolean.Companion|null[0] + final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborBoolean.Companion.serializer|serializer(){}[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] @@ -91,6 +101,18 @@ final class kotlinx.serialization.cbor/CborBuilder { // kotlinx.serialization.cb final fun (kotlin/Boolean) // kotlinx.serialization.cbor/CborBuilder.verifyValueTags.|(kotlin.Boolean){}[0] } +final class kotlinx.serialization.cbor/CborByteString : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborByteString|null[0] + constructor (kotlin/ByteArray, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborByteString.|(kotlin.ByteArray;kotlin.ULongArray...){}[0] + + final fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborByteString.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborByteString.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // kotlinx.serialization.cbor/CborByteString.toString|toString(){}[0] + + final object Companion { // kotlinx.serialization.cbor/CborByteString.Companion|null[0] + final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborByteString.Companion.serializer|serializer(){}[0] + } +} + final class kotlinx.serialization.cbor/CborConfiguration { // kotlinx.serialization.cbor/CborConfiguration|null[0] final val alwaysUseByteString // kotlinx.serialization.cbor/CborConfiguration.alwaysUseByteString|{}alwaysUseByteString[0] final fun (): kotlin/Boolean // kotlinx.serialization.cbor/CborConfiguration.alwaysUseByteString.|(){}[0] @@ -118,12 +140,133 @@ final class kotlinx.serialization.cbor/CborConfiguration { // kotlinx.serializat final fun toString(): kotlin/String // kotlinx.serialization.cbor/CborConfiguration.toString|toString(){}[0] } +final class kotlinx.serialization.cbor/CborDouble : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborDouble|null[0] + constructor (kotlin/Double, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborDouble.|(kotlin.Double;kotlin.ULongArray...){}[0] + + final object Companion { // kotlinx.serialization.cbor/CborDouble.Companion|null[0] + final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborDouble.Companion.serializer|serializer(){}[0] + } +} + +final class kotlinx.serialization.cbor/CborList : kotlin.collections/List, kotlinx.serialization.cbor/CborElement { // kotlinx.serialization.cbor/CborList|null[0] + constructor (kotlin.collections/List, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborList.|(kotlin.collections.List;kotlin.ULongArray...){}[0] + + final val size // kotlinx.serialization.cbor/CborList.size|{}size[0] + final fun (): kotlin/Int // kotlinx.serialization.cbor/CborList.size.|(){}[0] + + final fun contains(kotlinx.serialization.cbor/CborElement): kotlin/Boolean // kotlinx.serialization.cbor/CborList.contains|contains(kotlinx.serialization.cbor.CborElement){}[0] + final fun containsAll(kotlin.collections/Collection): kotlin/Boolean // kotlinx.serialization.cbor/CborList.containsAll|containsAll(kotlin.collections.Collection){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborList.equals|equals(kotlin.Any?){}[0] + final fun get(kotlin/Int): kotlinx.serialization.cbor/CborElement // kotlinx.serialization.cbor/CborList.get|get(kotlin.Int){}[0] + final fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborList.hashCode|hashCode(){}[0] + final fun indexOf(kotlinx.serialization.cbor/CborElement): kotlin/Int // kotlinx.serialization.cbor/CborList.indexOf|indexOf(kotlinx.serialization.cbor.CborElement){}[0] + final fun isEmpty(): kotlin/Boolean // kotlinx.serialization.cbor/CborList.isEmpty|isEmpty(){}[0] + final fun iterator(): kotlin.collections/Iterator // kotlinx.serialization.cbor/CborList.iterator|iterator(){}[0] + final fun lastIndexOf(kotlinx.serialization.cbor/CborElement): kotlin/Int // kotlinx.serialization.cbor/CborList.lastIndexOf|lastIndexOf(kotlinx.serialization.cbor.CborElement){}[0] + final fun listIterator(): kotlin.collections/ListIterator // kotlinx.serialization.cbor/CborList.listIterator|listIterator(){}[0] + final fun listIterator(kotlin/Int): kotlin.collections/ListIterator // kotlinx.serialization.cbor/CborList.listIterator|listIterator(kotlin.Int){}[0] + final fun subList(kotlin/Int, kotlin/Int): kotlin.collections/List // kotlinx.serialization.cbor/CborList.subList|subList(kotlin.Int;kotlin.Int){}[0] + final fun toString(): kotlin/String // kotlinx.serialization.cbor/CborList.toString|toString(){}[0] + + final object Companion { // kotlinx.serialization.cbor/CborList.Companion|null[0] + final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborList.Companion.serializer|serializer(){}[0] + } + + // Targets: [js] + final fun asJsReadonlyArrayView(): kotlin.js.collections/JsReadonlyArray // kotlinx.serialization.cbor/CborList.asJsReadonlyArrayView|asJsReadonlyArrayView(){}[0] +} + +final class kotlinx.serialization.cbor/CborMap : kotlin.collections/Map, kotlinx.serialization.cbor/CborElement { // kotlinx.serialization.cbor/CborMap|null[0] + constructor (kotlin.collections/Map, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborMap.|(kotlin.collections.Map;kotlin.ULongArray...){}[0] + + final val entries // kotlinx.serialization.cbor/CborMap.entries|{}entries[0] + final fun (): kotlin.collections/Set> // kotlinx.serialization.cbor/CborMap.entries.|(){}[0] + final val keys // kotlinx.serialization.cbor/CborMap.keys|{}keys[0] + final fun (): kotlin.collections/Set // kotlinx.serialization.cbor/CborMap.keys.|(){}[0] + final val size // kotlinx.serialization.cbor/CborMap.size|{}size[0] + final fun (): kotlin/Int // kotlinx.serialization.cbor/CborMap.size.|(){}[0] + final val values // kotlinx.serialization.cbor/CborMap.values|{}values[0] + final fun (): kotlin.collections/Collection // kotlinx.serialization.cbor/CborMap.values.|(){}[0] + + final fun containsKey(kotlinx.serialization.cbor/CborElement): kotlin/Boolean // kotlinx.serialization.cbor/CborMap.containsKey|containsKey(kotlinx.serialization.cbor.CborElement){}[0] + final fun containsValue(kotlinx.serialization.cbor/CborElement): kotlin/Boolean // kotlinx.serialization.cbor/CborMap.containsValue|containsValue(kotlinx.serialization.cbor.CborElement){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborMap.equals|equals(kotlin.Any?){}[0] + final fun get(kotlinx.serialization.cbor/CborElement): kotlinx.serialization.cbor/CborElement? // kotlinx.serialization.cbor/CborMap.get|get(kotlinx.serialization.cbor.CborElement){}[0] + final fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborMap.hashCode|hashCode(){}[0] + final fun isEmpty(): kotlin/Boolean // kotlinx.serialization.cbor/CborMap.isEmpty|isEmpty(){}[0] + final fun toString(): kotlin/String // kotlinx.serialization.cbor/CborMap.toString|toString(){}[0] + + final object Companion { // kotlinx.serialization.cbor/CborMap.Companion|null[0] + final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborMap.Companion.serializer|serializer(){}[0] + } + + // Targets: [js] + final fun asJsReadonlyMapView(): kotlin.js.collections/JsReadonlyMap // kotlinx.serialization.cbor/CborMap.asJsReadonlyMapView|asJsReadonlyMapView(){}[0] +} + +final class kotlinx.serialization.cbor/CborNegativeInt : kotlinx.serialization.cbor/CborInt { // kotlinx.serialization.cbor/CborNegativeInt|null[0] + constructor (kotlin/Long, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborNegativeInt.|(kotlin.Long;kotlin.ULongArray...){}[0] + + final object Companion { // kotlinx.serialization.cbor/CborNegativeInt.Companion|null[0] + final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborNegativeInt.Companion.serializer|serializer(){}[0] + } +} + +final class kotlinx.serialization.cbor/CborNull : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborNull|null[0] + constructor (kotlin/ULongArray...) // kotlinx.serialization.cbor/CborNull.|(kotlin.ULongArray...){}[0] + + final object Companion { // kotlinx.serialization.cbor/CborNull.Companion|null[0] + final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborNull.Companion.serializer|serializer(){}[0] + } +} + +final class kotlinx.serialization.cbor/CborPositiveInt : kotlinx.serialization.cbor/CborInt { // kotlinx.serialization.cbor/CborPositiveInt|null[0] + constructor (kotlin/ULong, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborPositiveInt.|(kotlin.ULong;kotlin.ULongArray...){}[0] + + final object Companion { // kotlinx.serialization.cbor/CborPositiveInt.Companion|null[0] + final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborPositiveInt.Companion.serializer|serializer(){}[0] + } +} + +final class kotlinx.serialization.cbor/CborString : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborString|null[0] + constructor (kotlin/String, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborString.|(kotlin.String;kotlin.ULongArray...){}[0] + + final object Companion { // kotlinx.serialization.cbor/CborString.Companion|null[0] + final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborString.Companion.serializer|serializer(){}[0] + } +} + +sealed class <#A: kotlin/Any> kotlinx.serialization.cbor/CborInt : kotlinx.serialization.cbor/CborPrimitive<#A> { // kotlinx.serialization.cbor/CborInt|null[0] + final object Companion : kotlinx.serialization.internal/SerializerFactory { // kotlinx.serialization.cbor/CborInt.Companion|null[0] + final fun <#A2: kotlin/Any?> serializer(kotlinx.serialization/KSerializer<#A2>): kotlinx.serialization/KSerializer> // kotlinx.serialization.cbor/CborInt.Companion.serializer|serializer(kotlinx.serialization.KSerializer<0:0>){0§}[0] + final fun invoke(kotlin/Long, kotlin/ULongArray...): kotlinx.serialization.cbor/CborInt<*> // kotlinx.serialization.cbor/CborInt.Companion.invoke|invoke(kotlin.Long;kotlin.ULongArray...){}[0] + final fun invoke(kotlin/ULong, kotlin/ULongArray...): kotlinx.serialization.cbor/CborInt // kotlinx.serialization.cbor/CborInt.Companion.invoke|invoke(kotlin.ULong;kotlin.ULongArray...){}[0] + final fun serializer(kotlin/Array>...): kotlinx.serialization/KSerializer<*> // kotlinx.serialization.cbor/CborInt.Companion.serializer|serializer(kotlin.Array>...){}[0] + } +} + +sealed class <#A: kotlin/Any> kotlinx.serialization.cbor/CborPrimitive : kotlinx.serialization.cbor/CborElement { // kotlinx.serialization.cbor/CborPrimitive|null[0] + final val value // kotlinx.serialization.cbor/CborPrimitive.value|{}value[0] + final fun (): #A // kotlinx.serialization.cbor/CborPrimitive.value.|(){}[0] + + open fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborPrimitive.equals|equals(kotlin.Any?){}[0] + open fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborPrimitive.hashCode|hashCode(){}[0] + open fun toString(): kotlin/String // kotlinx.serialization.cbor/CborPrimitive.toString|toString(){}[0] + + final object Companion : kotlinx.serialization.internal/SerializerFactory { // kotlinx.serialization.cbor/CborPrimitive.Companion|null[0] + final fun <#A2: kotlin/Any?> serializer(kotlinx.serialization/KSerializer<#A2>): kotlinx.serialization/KSerializer> // kotlinx.serialization.cbor/CborPrimitive.Companion.serializer|serializer(kotlinx.serialization.KSerializer<0:0>){0§}[0] + final fun serializer(kotlin/Array>...): kotlinx.serialization/KSerializer<*> // kotlinx.serialization.cbor/CborPrimitive.Companion.serializer|serializer(kotlin.Array>...){}[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?> decodeFromCborElement(kotlinx.serialization/DeserializationStrategy<#A1>, kotlinx.serialization.cbor/CborElement): #A1 // kotlinx.serialization.cbor/Cbor.decodeFromCborElement|decodeFromCborElement(kotlinx.serialization.DeserializationStrategy<0:0>;kotlinx.serialization.cbor.CborElement){0§}[0] + final fun <#A1: kotlin/Any?> encodeToCborElement(kotlinx.serialization/SerializationStrategy<#A1>, #A1): kotlinx.serialization.cbor/CborElement // kotlinx.serialization.cbor/Cbor.encodeToCborElement|encodeToCborElement(kotlinx.serialization.SerializationStrategy<0:0>;0:0){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] @@ -133,6 +276,18 @@ sealed class kotlinx.serialization.cbor/Cbor : kotlinx.serialization/BinaryForma } } +sealed class kotlinx.serialization.cbor/CborElement { // kotlinx.serialization.cbor/CborElement|null[0] + final var tags // kotlinx.serialization.cbor/CborElement.tags|{}tags[0] + final fun (): kotlin/ULongArray // kotlinx.serialization.cbor/CborElement.tags.|(){}[0] + + open fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborElement.equals|equals(kotlin.Any?){}[0] + open fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborElement.hashCode|hashCode(){}[0] + + final object Companion { // kotlinx.serialization.cbor/CborElement.Companion|null[0] + final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborElement.Companion.serializer|serializer(){}[0] + } +} + final object kotlinx.serialization.cbor/CborTag { // kotlinx.serialization.cbor/CborTag|null[0] final const val BASE16 // kotlinx.serialization.cbor/CborTag.BASE16|{}BASE16[0] final fun (): kotlin/ULong // kotlinx.serialization.cbor/CborTag.BASE16.|(){}[0] @@ -171,3 +326,5 @@ final object kotlinx.serialization.cbor/CborTag { // kotlinx.serialization.cbor/ } final fun kotlinx.serialization.cbor/Cbor(kotlinx.serialization.cbor/Cbor = ..., kotlin/Function1): kotlinx.serialization.cbor/Cbor // kotlinx.serialization.cbor/Cbor|Cbor(kotlinx.serialization.cbor.Cbor;kotlin.Function1){}[0] +final inline fun <#A: reified kotlin/Any?> (kotlinx.serialization.cbor/Cbor).kotlinx.serialization.cbor/decodeFromCborElement(kotlinx.serialization.cbor/CborElement): #A // kotlinx.serialization.cbor/decodeFromCborElement|decodeFromCborElement@kotlinx.serialization.cbor.Cbor(kotlinx.serialization.cbor.CborElement){0§}[0] +final inline fun <#A: reified kotlin/Any?> (kotlinx.serialization.cbor/Cbor).kotlinx.serialization.cbor/encodeToCborElement(#A): kotlinx.serialization.cbor/CborElement // kotlinx.serialization.cbor/encodeToCborElement|encodeToCborElement@kotlinx.serialization.cbor.Cbor(0:0){0§}[0] From 96f2004a8f52f4873e6842b743818a54481314e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Fri, 8 Aug 2025 17:45:50 +0200 Subject: [PATCH 25/68] docs + apidump --- docs/formats.md | 133 +++++++++++++++++- docs/serialization-guide.md | 5 + .../cbor/api/kotlinx-serialization-cbor.api | 18 +-- .../api/kotlinx-serialization-cbor.klib.api | 8 +- .../kotlinx/serialization/cbor/CborElement.kt | 18 ++- .../cbor/internal/CborElementSerializers.kt | 12 +- .../cbor/internal/CborTreeReader.kt | 2 +- .../serialization/cbor/internal/Decoder.kt | 2 +- .../serialization/cbor/internal/Encoder.kt | 4 +- .../cbor/CborElementEqualityTest.kt | 12 +- .../serialization/cbor/CborElementTest.kt | 4 +- .../kotlinx/serialization/cbor/CborIsoTest.kt | 1 + guide/example/example-formats-04.kt | 4 + guide/test/FormatsTest.kt | 7 + 14 files changed, 188 insertions(+), 42 deletions(-) diff --git a/docs/formats.md b/docs/formats.md index ec4e6cb820..010d2a331a 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -17,6 +17,11 @@ stable, these are currently experimental features of Kotlin Serialization. * [Tags and Labels](#tags-and-labels) * [Arrays](#arrays) * [Custom CBOR-specific Serializers](#custom-cbor-specific-serializers) + * [CBOR Elements](#cbor-elements) + * [Encoding from/to `CborElement`](#encoding-fromto-cborelement) + * [Tagging `CborElement`s](#tagging-cborelements) + * [Caution](#caution) + * [Types of CBOR Elements](#types-of-cbor-elements) * [ProtoBuf (experimental)](#protobuf-experimental) * [Field numbers](#field-numbers) * [Integer types](#integer-types) @@ -308,13 +313,125 @@ When annotated with `@CborArray`, serialization of the same object will produce ``` This may be used to encode COSE structures, see [RFC 9052 2. Basic COSE Structure](https://www.rfc-editor.org/rfc/rfc9052#section-2). - ### Custom CBOR-specific Serializers Cbor encoders and decoders implement the interfaces [CborEncoder](CborEncoder.kt) and [CborDecoder](CborDecoder.kt), respectively. These interfaces contain a single property, `cbor`, exposing the current CBOR serialization configuration. This enables custom cbor-specific serializers to reuse the current `Cbor` instance to produce embedded byte arrays or react to configuration settings such as `preferCborLabelsOverNames` or `useDefiniteLengthEncoding`, for example. + +### CBOR Elements + +Aside from direct conversions between bytearray and CBOR objects, Kotlin serialization offers APIs that allow +other ways of working with CBOR in the code. For example, you might need to tweak the data before it can parse +or otherwise work with such unstructured data that it does not readily fit into the typesafe world of Kotlin +serialization. + +The main concept in this part of the library is [CborElement]. Read on to learn what you can do with it. + +#### Encoding from/to `CborElement` + +Bytes can be decoded into an instance of `CborElement` with the [Cbor.decodeFromByteArray] function by either manually +specifying [CborElement.serializer()] or specifying [CborElement] as generic type parameter. +It is also possible to encode arbitrary serializable structures to a `CborElement` through [Cbor.encodeToCborElement]. + +Since these operations use the same code paths as regular serialization (but with specialized serializers), the config flags +behave as expected: + +```kotlin +fun main() { + val element: CborElement = Cbor.decodeFromHexString("a165627974657343666f6f") + println(element) +} +``` + +The above snippet will print the following diagnostic notation + +```text +CborMap(tags=[], content={CborString(tags=[], value=bytes)=CborByteString(tags=[], value=h'666f6f)}) +``` + +#### Tagging `CborElement`s + +Every CborElement—whether it is used as a property, a value inside a collection, or even a complex key inside a map +(which is perfectly legal in CBOR)—supports tags. Tags can be specified by passing them s varargs parameters upon +CborElement creation. +For example, take following structure (represented in diagnostic notation): + + + +```hexdump +bf # map(*) + 61 # text(1) + 61 # "a" + cc # tag(12) + 1a 0fffffff # unsigned(268,435,455) + d8 22 # base64 encoded text, tag(34) + 61 # text(1) + 62 # "b" + # invalid length at 0 for base64 + 20 # negative(-1) + d8 38 # tag(56) + 61 # text(1) + 63 # "c" + d8 4e # typed array of i32, little endian, twos-complement, tag(78) + 42 # bytes(2) + cafe # "\xca\xfe" + # invalid data length for typed array + 61 # text(1) + 64 # "d" + d8 5a # tag(90) + cc # tag(12) + 6b # text(11) + 48656c6c6f20576f726c64 # "Hello World" + ff # break +``` + +Decoding it results in the following CborElement (shown in manually formatted diagnostic notation): + +``` +CborMap(tags=[], content={ + CborString(tags=[], value=a) = CborPositiveInt( tags=[12], value=268435455), + CborString(tags=[34], value=b) = CborNegativeInt( tags=[], value=-1), + CborString(tags=[56], value=c) = CborByteString( tags=[78], value=h'cafe), + CborString(tags=[], value=d) = CborString( tags=[90, 12], value=Hello World) +}) +``` + +##### Caution + +Tags are properties of `CborElements`, and it is possible to mixing arbitrary serializable values with `CborElement`s that +contain tags inside a serializable structure. It is also possible to annotate any [CborElement] property +of a generic serializable class with `@ValueTags`. +**This can lead to asymmetric behavior when serializing and deserializing such structures!** + +#### Types of CBOR Elements + +A [CborElement] class has three direct subtypes, closely following CBOR grammar: + +* [CborPrimitive] represents primitive CBOR elements, such as string, integer, float boolean, and null. + CBOR byte strings are also treated as primitives + Each primitive has a [value][CborPrimitive.value]. Depending on the concrete type of the primitive, it maps + to corresponding Kotlin Types such as `String`, `Int`, `Double`, etc. + Note that Cbor discriminates between positive ("unsigned") and negative ("signed") integers! + `CborPrimitive` is itself an umbrella type (a sealed class) for the following concrete primitives: + * [CborNull] mapping to a Kotlin `null` + * [CborBoolean] mapping to a Kotlin `Boolean` + * [CborInt] which is an umbrella type (a sealed class) itself for the following concrete types + (it is still possible to instantiate it as the `invoke` operator on its companion is overridden accordingly): + * [CborPositiveInt] represents all `Long` numbers `≥0` + * [CborNegativeInt] represents all `Long` numbers `<0` + * [CborString] maps to a Kotlin `String` + * [CborFloat] maps to Kotlin `Double` + * [CborByteString] maps to a Kotlin `ByteArray` and is used to encode them as CBOR byte string (in contrast to a list + of individual bytes) + +* [CborList] represents a CBOR array. It is a Kotlin [List] of `CborElement` items. + +* [CborMap] represents a CBOR map/object. It is a Kotlin [Map] from `CborElement` keys to `CborElement` values. + This is typically the result of serializing an arbitrary + + ## ProtoBuf (experimental) [Protocol Buffers](https://developers.google.com/protocol-buffers) is a language-neutral binary format that normally @@ -1673,5 +1790,19 @@ This chapter concludes [Kotlin Serialization Guide](serialization-guide.md). [Cbor.decodeFromByteArray]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor/decode-from-byte-array.html [CborBuilder.ignoreUnknownKeys]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-builder/ignore-unknown-keys.html [ByteString]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-byte-string/index.html +[CborElement]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-element/index.html +[Cbor.encodeToCborElement]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/encode-to-cbor-element.html +[CborPrimitive]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-primitive/index.html +[CborPrimitive.value]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-primitive/value.html +[CborNull]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-null/index.html +[CborBoolean]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-boolean/index.html +[CborInt]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-int/index.html +[CborPositiveInt]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-positive-int/index.html +[CborNegativeInt]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-negative-int/index.html +[CborString]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-string/index.html +[CborFloat]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-float/index.html +[CborByteString]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-byte-string/index.html +[CborList]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-list/index.html +[CborMap]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-map/index.html diff --git a/docs/serialization-guide.md b/docs/serialization-guide.md index c01eb6231c..d03f518f01 100644 --- a/docs/serialization-guide.md +++ b/docs/serialization-guide.md @@ -155,6 +155,11 @@ Once the project is set up, we can start serializing some classes. * [Tags and Labels](formats.md#tags-and-labels) * [Arrays](formats.md#arrays) * [Custom CBOR-specific Serializers](formats.md#custom-cbor-specific-serializers) + * [CBOR Elements](formats.md#cbor-elements) + * [Encoding from/to `CborElement`](formats.md#encoding-fromto-cborelement) + * [Tagging `CborElement`s](formats.md#tagging-cborelements) + * [Caution](formats.md#caution) + * [Types of CBOR Elements](formats.md#types-of-cbor-elements) * [ProtoBuf (experimental)](formats.md#protobuf-experimental) * [Field numbers](formats.md#field-numbers) * [Integer types](formats.md#integer-types) diff --git a/formats/cbor/api/kotlinx-serialization-cbor.api b/formats/cbor/api/kotlinx-serialization-cbor.api index 131b13e1ba..9b42d2a8b6 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.api @@ -100,15 +100,6 @@ public final class kotlinx/serialization/cbor/CborDecoder$DefaultImpls { public static fun decodeSerializableValue (Lkotlinx/serialization/cbor/CborDecoder;Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object; } -public final class kotlinx/serialization/cbor/CborDouble : kotlinx/serialization/cbor/CborPrimitive { - public static final field Companion Lkotlinx/serialization/cbor/CborDouble$Companion; - public synthetic fun (D[JLkotlin/jvm/internal/DefaultConstructorMarker;)V -} - -public final class kotlinx/serialization/cbor/CborDouble$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - public abstract class kotlinx/serialization/cbor/CborElement { public static final field Companion Lkotlinx/serialization/cbor/CborElement$Companion; public synthetic fun ([JILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -133,6 +124,15 @@ public final class kotlinx/serialization/cbor/CborEncoder$DefaultImpls { public static fun encodeSerializableValue (Lkotlinx/serialization/cbor/CborEncoder;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)V } +public final class kotlinx/serialization/cbor/CborFloat : kotlinx/serialization/cbor/CborPrimitive { + public static final field Companion Lkotlinx/serialization/cbor/CborFloat$Companion; + public synthetic fun (D[JLkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class kotlinx/serialization/cbor/CborFloat$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public abstract class kotlinx/serialization/cbor/CborInt : kotlinx/serialization/cbor/CborPrimitive { public static final field Companion Lkotlinx/serialization/cbor/CborInt$Companion; public synthetic fun ([JLjava/lang/Object;ILkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/formats/cbor/api/kotlinx-serialization-cbor.klib.api b/formats/cbor/api/kotlinx-serialization-cbor.klib.api index 6d5bce0a15..cb5d83daa1 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.klib.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.klib.api @@ -140,11 +140,11 @@ final class kotlinx.serialization.cbor/CborConfiguration { // kotlinx.serializat final fun toString(): kotlin/String // kotlinx.serialization.cbor/CborConfiguration.toString|toString(){}[0] } -final class kotlinx.serialization.cbor/CborDouble : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborDouble|null[0] - constructor (kotlin/Double, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborDouble.|(kotlin.Double;kotlin.ULongArray...){}[0] +final class kotlinx.serialization.cbor/CborFloat : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborFloat|null[0] + constructor (kotlin/Double, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborFloat.|(kotlin.Double;kotlin.ULongArray...){}[0] - final object Companion { // kotlinx.serialization.cbor/CborDouble.Companion|null[0] - final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborDouble.Companion.serializer|serializer(){}[0] + final object Companion { // kotlinx.serialization.cbor/CborFloat.Companion|null[0] + final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborFloat.Companion.serializer|serializer(){}[0] } } diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt index b33a18ac9f..bb89c1e020 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -81,9 +81,8 @@ public sealed class CborPrimitive( } override fun toString(): String { - return "CborPrimitive(" + - "kind=${value::class.simpleName}, " + - "tags=${tags.joinToString()}, " + + return "${this::class.simpleName}(" + + "tags=${tags.joinToString(prefix = "[", postfix = "]")}, " + "value=$value" + ")" } @@ -150,8 +149,8 @@ public class CborPositiveInt( /** * Class representing CBOR floating point value (major type 7). */ -@Serializable(with = CborDoubleSerializer::class) -public class CborDouble( +@Serializable(with = CborFloatSerializer::class) +public class CborFloat( value: Double, vararg tags: ULong ) : CborPrimitive(value, tags) @@ -196,9 +195,8 @@ public class CborByteString( } override fun toString(): String { - return "CborPrimitive(" + - "kind=${value::class.simpleName}, " + - "tags=${tags.joinToString()}, " + + return "CborByteString(" + + "tags=${tags.joinToString(prefix = "[", postfix = "]")}, " + "value=h'${value.toHexString()}" + ")" } @@ -229,7 +227,7 @@ public class CborMap( override fun toString(): String { return "CborMap(" + - "tags=${tags.joinToString()}, " + + "tags=${tags.joinToString(prefix = "[", postfix = "]")}, " + "content=$content" + ")" } @@ -255,7 +253,7 @@ public class CborList( override fun toString(): String { return "CborList(" + - "tags=${tags.joinToString()}, " + + "tags=${tags.joinToString(prefix = "[", postfix = "]")}, " + "content=$content" + ")" } diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index 53d72655c7..d466fcf142 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -28,7 +28,7 @@ internal object CborElementSerializer : KSerializer, CborSerializer element("CborByteString", defer { CborByteStringSerializer.descriptor }) element("CborMap", defer { CborMapSerializer.descriptor }) element("CborList", defer { CborListSerializer.descriptor }) - element("CborDouble", defer { CborDoubleSerializer.descriptor }) + element("CborDouble", defer { CborFloatSerializer.descriptor }) element("CborInt", defer { CborNegativeIntSerializer.descriptor }) element("CborUInt", defer { CborPositiveIntSerializer.descriptor }) } @@ -64,7 +64,7 @@ internal object CborPrimitiveSerializer : KSerializer>, CborSer is CborString -> encoder.encodeSerializableValue(CborStringSerializer, value) is CborBoolean -> encoder.encodeSerializableValue(CborBooleanSerializer, value) is CborByteString -> encoder.encodeSerializableValue(CborByteStringSerializer, value) - is CborDouble -> encoder.encodeSerializableValue(CborDoubleSerializer, value) + is CborFloat -> encoder.encodeSerializableValue(CborFloatSerializer, value) is CborNegativeInt -> encoder.encodeSerializableValue(CborNegativeIntSerializer, value) is CborPositiveInt -> encoder.encodeSerializableValue(CborPositiveIntSerializer, value) } @@ -154,19 +154,19 @@ internal object CborPositiveIntSerializer : KSerializer, CborSe } } -internal object CborDoubleSerializer : KSerializer, CborSerializer { +internal object CborFloatSerializer : KSerializer, CborSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborDouble", PrimitiveKind.DOUBLE) - override fun serialize(encoder: Encoder, value: CborDouble) { + override fun serialize(encoder: Encoder, value: CborFloat) { val cborEncoder = encoder.asCborEncoder() cborEncoder.encodeTags(value) encoder.encodeDouble(value.value) } - override fun deserialize(decoder: Decoder): CborDouble { + override fun deserialize(decoder: Decoder): CborFloat { decoder.asCborDecoder() - return CborDouble(decoder.decodeDouble()) + return CborFloat(decoder.decodeDouble()) } } diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt index 1deeaf78b5..d86295e7e3 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt @@ -69,7 +69,7 @@ internal class CborTreeReader( CborNull(tags = tags) } // Half/Float32/Float64 - NEXT_HALF, NEXT_FLOAT, NEXT_DOUBLE -> CborDouble(parser.nextDouble(), tags = tags) + NEXT_HALF, NEXT_FLOAT, NEXT_DOUBLE -> CborFloat(parser.nextDouble(), tags = tags) else -> throw CborDecodingException( "Invalid simple value or float type: ${parser.curByte.toString(16).uppercase()}" ) 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 624b190864..30584e93c1 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -693,7 +693,7 @@ internal class StructuredCborParser(internal val element: CborElement, private v override fun nextDouble(tags: ULongArray?): Double { processTags(tags) return when (layer.current) { - is CborDouble -> (layer.current as CborDouble).value + is CborFloat -> (layer.current as CborFloat).value else -> throw CborDecodingException("Expected double, got ${layer.current::class.simpleName}") } } 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 6cc67b2a21..040964c98f 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -331,11 +331,11 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { } override fun encodeDouble(value: Double) { - currentElement += CborDouble(value, tags = nextValueTags) + currentElement += CborFloat(value, tags = nextValueTags) } override fun encodeFloat(value: Float) { - currentElement += CborDouble(value.toDouble(), tags = nextValueTags) + currentElement += CborFloat(value.toDouble(), tags = nextValueTags) } override fun encodeInt(value: Int) { diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt index 911b8c6808..40bf07f893 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt @@ -51,10 +51,10 @@ class CborElementEqualityTest { @Test fun testCborDoubleEquality() { - val double1 = CborDouble(3.14) - val double2 = CborDouble(3.14) - val double3 = CborDouble(2.71) - val double4 = CborDouble(3.14, 1u) + val double1 = CborFloat(3.14) + val double2 = CborFloat(3.14) + val double3 = CborFloat(2.71) + val double4 = CborFloat(3.14, 1u) assertEquals(double1, double2) assertEquals(double1.hashCode(), double2.hashCode()) @@ -264,7 +264,7 @@ class CborElementEqualityTest { val elements = listOf( CborPositiveInt(42u), CborNegativeInt(-42), - CborDouble(3.14), + CborFloat(3.14), CborString("test"), CborBoolean(true), CborByteString(byteArrayOf(1, 2, 3)), @@ -284,7 +284,7 @@ class CborElementEqualityTest { val pairs = listOf( CborPositiveInt(42u) to CborPositiveInt(42u), CborNegativeInt(-42) to CborNegativeInt(-42), - CborDouble(3.14) to CborDouble(3.14), + CborFloat(3.14) to CborFloat(3.14), CborString("test") to CborString("test"), CborBoolean(true) to CborBoolean(true), CborByteString(byteArrayOf(1, 2, 3)) to CborByteString(byteArrayOf(1, 2, 3)), diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index ad060a5b81..102c5f911e 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -233,11 +233,11 @@ class CborElementTest { fun testDecodeFloatingPoint() { // Test data from CborParserTest.testParseDoubles val doubleElement = cbor.decodeFromHexString("fb7e37e43c8800759c") - assertTrue(doubleElement is CborDouble) + assertTrue(doubleElement is CborFloat) assertEquals(1e+300, doubleElement.value) val floatElement = cbor.decodeFromHexString("fa47c35000") - assertTrue(floatElement is CborDouble) + assertTrue(floatElement is CborFloat) assertEquals(100000.0f, floatElement.value.toFloat()) } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborIsoTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborIsoTest.kt index 1992cd0331..639d69a22a 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborIsoTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborIsoTest.kt @@ -29,6 +29,7 @@ class CborIsoTest { assertEquals(referenceHexString, cbor.encodeToHexString(DataClass.serializer(), reference)) val struct = cbor.encodeToCborElement(DataClass.serializer(), reference) + assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), referenceHexString)) assertEquals(reference, cbor.decodeFromCborElement(DataClass.serializer(), struct)) assertEquals(referenceHexString, cbor.encodeToHexString(CborElement.serializer(), struct)) } diff --git a/guide/example/example-formats-04.kt b/guide/example/example-formats-04.kt index 0ff0289feb..474927db86 100644 --- a/guide/example/example-formats-04.kt +++ b/guide/example/example-formats-04.kt @@ -1,6 +1,10 @@ // This file was automatically generated from formats.md by Knit tool. Do not edit. package example.exampleFormats04 +fun main() { + val element: CborElement = Cbor.decodeFromHexString("a165627974657343666f6f") + println(element) +} import kotlinx.serialization.* import kotlinx.serialization.protobuf.* diff --git a/guide/test/FormatsTest.kt b/guide/test/FormatsTest.kt index f305a59bf2..e8c84c77ff 100644 --- a/guide/test/FormatsTest.kt +++ b/guide/test/FormatsTest.kt @@ -28,6 +28,13 @@ class FormatsTest { ) } + @Test + fun testExampleFormats03() { + captureOutput("ExampleFormats03") { example.exampleFormats03.main() }.verifyOutputLines( + "CborMap(tags=[], content={CborString(tags=[], value=bytes)=CborByteString(tags=[], value=h'666f6f)})" + ) + } + @Test fun testExampleFormats04() { captureOutput("ExampleFormats04") { example.exampleFormats04.main() }.verifyOutputLines( From 7e377bedbb3d280a34694bffd085547c11433ff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Tue, 26 Aug 2025 17:18:44 +0200 Subject: [PATCH 26/68] refactor cborint --- docs/formats.md | 14 +- .../cbor/api/kotlinx-serialization-cbor.api | 39 ++--- .../api/kotlinx-serialization-cbor.klib.api | 55 ++++--- .../kotlinx/serialization/cbor/CborElement.kt | 93 +++++++---- .../cbor/internal/CborElementSerializers.kt | 58 ++----- .../cbor/internal/CborTreeReader.kt | 8 +- .../serialization/cbor/internal/Decoder.kt | 33 ++-- .../serialization/cbor/internal/Encoder.kt | 21 ++- .../serialization/cbor/CborArrayTest.kt | 2 +- .../cbor/CborElementEqualityTest.kt | 70 ++++---- .../serialization/cbor/CborElementTest.kt | 150 ++++++++++++++---- 11 files changed, 328 insertions(+), 215 deletions(-) diff --git a/docs/formats.md b/docs/formats.md index 010d2a331a..76260daa1e 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -332,7 +332,7 @@ The main concept in this part of the library is [CborElement]. Read on to learn #### Encoding from/to `CborElement` Bytes can be decoded into an instance of `CborElement` with the [Cbor.decodeFromByteArray] function by either manually -specifying [CborElement.serializer()] or specifying [CborElement] as generic type parameter. +specifying `CborElement.serializer()` or specifying [CborElement] as generic type parameter. It is also possible to encode arbitrary serializable structures to a `CborElement` through [Cbor.encodeToCborElement]. Since these operations use the same code paths as regular serialization (but with specialized serializers), the config flags @@ -417,18 +417,16 @@ A [CborElement] class has three direct subtypes, closely following CBOR grammar: `CborPrimitive` is itself an umbrella type (a sealed class) for the following concrete primitives: * [CborNull] mapping to a Kotlin `null` * [CborBoolean] mapping to a Kotlin `Boolean` - * [CborInt] which is an umbrella type (a sealed class) itself for the following concrete types - (it is still possible to instantiate it as the `invoke` operator on its companion is overridden accordingly): - * [CborPositiveInt] represents all `Long` numbers `≥0` - * [CborNegativeInt] represents all `Long` numbers `<0` + * [CborInt] represents signed CBOR integer (major type 1 encompassing `-2^64..-1`) and unsigned CBOR integer (major type 0 encompassing `0..2^64-1`). + Since this exceeds the range of Kotlin's built-in `Long` type, CborInt consists of `sign` (set to `CborInt.Sing.POSITIVE`, `CborInt.Sing.NEGATIVE`, or `CborInt.Sing.ZERO`) and `value` representing the absolute value as an `ULong`. It also features a `toLong()` function, albeit incurring possible truncation for negative values exceeding `Long.MIN_VALUE`. * [CborString] maps to a Kotlin `String` * [CborFloat] maps to Kotlin `Double` * [CborByteString] maps to a Kotlin `ByteArray` and is used to encode them as CBOR byte string (in contrast to a list of individual bytes) -* [CborList] represents a CBOR array. It is a Kotlin [List] of `CborElement` items. +* [CborList] represents a CBOR array. It is a Kotlin `List` of `CborElement` items. -* [CborMap] represents a CBOR map/object. It is a Kotlin [Map] from `CborElement` keys to `CborElement` values. +* [CborMap] represents a CBOR map/object. It is a Kotlin `Map` from `CborElement` keys to `CborElement` values. This is typically the result of serializing an arbitrary @@ -1797,8 +1795,6 @@ This chapter concludes [Kotlin Serialization Guide](serialization-guide.md). [CborNull]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-null/index.html [CborBoolean]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-boolean/index.html [CborInt]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-int/index.html -[CborPositiveInt]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-positive-int/index.html -[CborNegativeInt]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-negative-int/index.html [CborString]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-string/index.html [CborFloat]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-float/index.html [CborByteString]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-byte-string/index.html diff --git a/formats/cbor/api/kotlinx-serialization-cbor.api b/formats/cbor/api/kotlinx-serialization-cbor.api index 9b42d2a8b6..1a638aab4c 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.api @@ -133,16 +133,29 @@ public final class kotlinx/serialization/cbor/CborFloat$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public abstract class kotlinx/serialization/cbor/CborInt : kotlinx/serialization/cbor/CborPrimitive { +public final class kotlinx/serialization/cbor/CborInt : kotlinx/serialization/cbor/CborPrimitive { public static final field Companion Lkotlinx/serialization/cbor/CborInt$Companion; - public synthetic fun ([JLjava/lang/Object;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun ([JLjava/lang/Object;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JLkotlinx/serialization/cbor/CborInt$Sign;[JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getSign ()Lkotlinx/serialization/cbor/CborInt$Sign; + public fun hashCode ()I + public final fun toLong ()J + public fun toString ()Ljava/lang/String; } public final class kotlinx/serialization/cbor/CborInt$Companion { public final fun invoke-SIFponk (J[J)Lkotlinx/serialization/cbor/CborInt; public final fun invoke-ahITK_k (J[J)Lkotlinx/serialization/cbor/CborInt; - public final fun serializer (Lkotlinx/serialization/KSerializer;)Lkotlinx/serialization/KSerializer; + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class kotlinx/serialization/cbor/CborInt$Sign : java/lang/Enum { + public static final field NEGATIVE Lkotlinx/serialization/cbor/CborInt$Sign; + public static final field POSITIVE Lkotlinx/serialization/cbor/CborInt$Sign; + public static final field ZERO Lkotlinx/serialization/cbor/CborInt$Sign; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lkotlinx/serialization/cbor/CborInt$Sign; + public static fun values ()[Lkotlinx/serialization/cbor/CborInt$Sign; } public final class kotlinx/serialization/cbor/CborKt { @@ -255,15 +268,6 @@ public final class kotlinx/serialization/cbor/CborMap$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class kotlinx/serialization/cbor/CborNegativeInt : kotlinx/serialization/cbor/CborInt { - public static final field Companion Lkotlinx/serialization/cbor/CborNegativeInt$Companion; - public synthetic fun (J[JLkotlin/jvm/internal/DefaultConstructorMarker;)V -} - -public final class kotlinx/serialization/cbor/CborNegativeInt$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - public final class kotlinx/serialization/cbor/CborNull : kotlinx/serialization/cbor/CborPrimitive { public static final field Companion Lkotlinx/serialization/cbor/CborNull$Companion; public synthetic fun ([JLkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -273,15 +277,6 @@ public final class kotlinx/serialization/cbor/CborNull$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class kotlinx/serialization/cbor/CborPositiveInt : kotlinx/serialization/cbor/CborInt { - public static final field Companion Lkotlinx/serialization/cbor/CborPositiveInt$Companion; - public synthetic fun (J[JLkotlin/jvm/internal/DefaultConstructorMarker;)V -} - -public final class kotlinx/serialization/cbor/CborPositiveInt$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - public abstract class kotlinx/serialization/cbor/CborPrimitive : kotlinx/serialization/cbor/CborElement { public static final field Companion Lkotlinx/serialization/cbor/CborPrimitive$Companion; public synthetic fun (Ljava/lang/Object;[JILkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/formats/cbor/api/kotlinx-serialization-cbor.klib.api b/formats/cbor/api/kotlinx-serialization-cbor.klib.api index cb5d83daa1..ae73a9cc79 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.klib.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.klib.api @@ -148,6 +148,36 @@ final class kotlinx.serialization.cbor/CborFloat : kotlinx.serialization.cbor/Cb } } +final class kotlinx.serialization.cbor/CborInt : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborInt|null[0] + constructor (kotlin/ULong, kotlinx.serialization.cbor/CborInt.Sign, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborInt.|(kotlin.ULong;kotlinx.serialization.cbor.CborInt.Sign;kotlin.ULongArray...){}[0] + + final val sign // kotlinx.serialization.cbor/CborInt.sign|{}sign[0] + final fun (): kotlinx.serialization.cbor/CborInt.Sign // kotlinx.serialization.cbor/CborInt.sign.|(){}[0] + + final fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborInt.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborInt.hashCode|hashCode(){}[0] + final fun toLong(): kotlin/Long // kotlinx.serialization.cbor/CborInt.toLong|toLong(){}[0] + final fun toString(): kotlin/String // kotlinx.serialization.cbor/CborInt.toString|toString(){}[0] + + final enum class Sign : kotlin/Enum { // kotlinx.serialization.cbor/CborInt.Sign|null[0] + enum entry NEGATIVE // kotlinx.serialization.cbor/CborInt.Sign.NEGATIVE|null[0] + enum entry POSITIVE // kotlinx.serialization.cbor/CborInt.Sign.POSITIVE|null[0] + enum entry ZERO // kotlinx.serialization.cbor/CborInt.Sign.ZERO|null[0] + + final val entries // kotlinx.serialization.cbor/CborInt.Sign.entries|#static{}entries[0] + final fun (): kotlin.enums/EnumEntries // kotlinx.serialization.cbor/CborInt.Sign.entries.|#static(){}[0] + + final fun valueOf(kotlin/String): kotlinx.serialization.cbor/CborInt.Sign // kotlinx.serialization.cbor/CborInt.Sign.valueOf|valueOf#static(kotlin.String){}[0] + final fun values(): kotlin/Array // kotlinx.serialization.cbor/CborInt.Sign.values|values#static(){}[0] + } + + final object Companion { // kotlinx.serialization.cbor/CborInt.Companion|null[0] + final fun invoke(kotlin/Long, kotlin/ULongArray...): kotlinx.serialization.cbor/CborInt // kotlinx.serialization.cbor/CborInt.Companion.invoke|invoke(kotlin.Long;kotlin.ULongArray...){}[0] + final fun invoke(kotlin/ULong, kotlin/ULongArray...): kotlinx.serialization.cbor/CborInt // kotlinx.serialization.cbor/CborInt.Companion.invoke|invoke(kotlin.ULong;kotlin.ULongArray...){}[0] + final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborInt.Companion.serializer|serializer(){}[0] + } +} + final class kotlinx.serialization.cbor/CborList : kotlin.collections/List, kotlinx.serialization.cbor/CborElement { // kotlinx.serialization.cbor/CborList|null[0] constructor (kotlin.collections/List, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborList.|(kotlin.collections.List;kotlin.ULongArray...){}[0] @@ -204,14 +234,6 @@ final class kotlinx.serialization.cbor/CborMap : kotlin.collections/Map // kotlinx.serialization.cbor/CborMap.asJsReadonlyMapView|asJsReadonlyMapView(){}[0] } -final class kotlinx.serialization.cbor/CborNegativeInt : kotlinx.serialization.cbor/CborInt { // kotlinx.serialization.cbor/CborNegativeInt|null[0] - constructor (kotlin/Long, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborNegativeInt.|(kotlin.Long;kotlin.ULongArray...){}[0] - - final object Companion { // kotlinx.serialization.cbor/CborNegativeInt.Companion|null[0] - final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborNegativeInt.Companion.serializer|serializer(){}[0] - } -} - final class kotlinx.serialization.cbor/CborNull : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborNull|null[0] constructor (kotlin/ULongArray...) // kotlinx.serialization.cbor/CborNull.|(kotlin.ULongArray...){}[0] @@ -220,14 +242,6 @@ final class kotlinx.serialization.cbor/CborNull : kotlinx.serialization.cbor/Cbo } } -final class kotlinx.serialization.cbor/CborPositiveInt : kotlinx.serialization.cbor/CborInt { // kotlinx.serialization.cbor/CborPositiveInt|null[0] - constructor (kotlin/ULong, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborPositiveInt.|(kotlin.ULong;kotlin.ULongArray...){}[0] - - final object Companion { // kotlinx.serialization.cbor/CborPositiveInt.Companion|null[0] - final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborPositiveInt.Companion.serializer|serializer(){}[0] - } -} - final class kotlinx.serialization.cbor/CborString : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborString|null[0] constructor (kotlin/String, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborString.|(kotlin.String;kotlin.ULongArray...){}[0] @@ -236,15 +250,6 @@ final class kotlinx.serialization.cbor/CborString : kotlinx.serialization.cbor/C } } -sealed class <#A: kotlin/Any> kotlinx.serialization.cbor/CborInt : kotlinx.serialization.cbor/CborPrimitive<#A> { // kotlinx.serialization.cbor/CborInt|null[0] - final object Companion : kotlinx.serialization.internal/SerializerFactory { // kotlinx.serialization.cbor/CborInt.Companion|null[0] - final fun <#A2: kotlin/Any?> serializer(kotlinx.serialization/KSerializer<#A2>): kotlinx.serialization/KSerializer> // kotlinx.serialization.cbor/CborInt.Companion.serializer|serializer(kotlinx.serialization.KSerializer<0:0>){0§}[0] - final fun invoke(kotlin/Long, kotlin/ULongArray...): kotlinx.serialization.cbor/CborInt<*> // kotlinx.serialization.cbor/CborInt.Companion.invoke|invoke(kotlin.Long;kotlin.ULongArray...){}[0] - final fun invoke(kotlin/ULong, kotlin/ULongArray...): kotlinx.serialization.cbor/CborInt // kotlinx.serialization.cbor/CborInt.Companion.invoke|invoke(kotlin.ULong;kotlin.ULongArray...){}[0] - final fun serializer(kotlin/Array>...): kotlinx.serialization/KSerializer<*> // kotlinx.serialization.cbor/CborInt.Companion.serializer|serializer(kotlin.Array>...){}[0] - } -} - sealed class <#A: kotlin/Any> kotlinx.serialization.cbor/CborPrimitive : kotlinx.serialization.cbor/CborElement { // kotlinx.serialization.cbor/CborPrimitive|null[0] final val value // kotlinx.serialization.cbor/CborPrimitive.value|{}value[0] final fun (): #A // kotlinx.serialization.cbor/CborPrimitive.value.|(){}[0] diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt index bb89c1e020..2d815d6061 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -90,29 +90,53 @@ public sealed class CborPrimitive( /** * Class representing either: - * * signed CBOR integer (major type 1) - * * unsigned CBOR integer (major type 0) + * * signed CBOR integer (major type 1 encompassing `-2^64..-1`) + * * unsigned CBOR integer (major type 0 encompassing `0..2^64-1`) * - * depending on whether a positive or a negative number was passed. + * depending on the value of [sign]. Note that [absoluteValue] **must** be `0` when sign is set to [Sign.ZERO] */ @Serializable(with = CborIntSerializer::class) -public sealed class CborInt( - tags: ULongArray = ulongArrayOf(), - value: T, -) : CborPrimitive(value, tags) { +public class CborInt( + absoluteValue: ULong, + public val sign: Sign, + vararg tags: ULong +) : CborPrimitive(absoluteValue, tags) { + + init { + if (sign == Sign.ZERO) require(absoluteValue == 0uL) { "Illegal absolute value $absoluteValue for Sign.ZERO" } + } + + public enum class Sign { + POSITIVE, + NEGATIVE, + ZERO + } + + /** + * **WARNING! Possible truncation/overflow!** E.g., `-2^64` -> `1` + */ + public fun toLong(): Long = when (sign) { + Sign.POSITIVE, Sign.ZERO -> value.toLong() + Sign.NEGATIVE -> -value.toLong() + } + + public companion object { /** * Creates: - * * signed CBOR integer (major type 1) - * * unsigned CBOR integer (major type 0) + * * signed CBOR integer (major type 1 encompassing `-2^64..-1`) + * * unsigned CBOR integer (major type 0 encompassing `0..2^64-1`) * * depending on whether a positive or a negative number was passed. + * If you want to create a negative number exceeding [Long.MIN_VALUE], manually specify sign: `CborInt(ULong.MAX_VALUE, CborInt.Sign.NEGATIVE)` */ public operator fun invoke( value: Long, vararg tags: ULong - ): CborInt<*> = - if (value >= 0) CborPositiveInt(value.toULong(), tags = tags) else CborNegativeInt(value, tags = tags) + ): CborInt = + if (value == 0L) CborInt(value.toULong(), Sign.ZERO, tags = tags) + else if (value > 0L) CborInt(value.toULong(), Sign.POSITIVE, tags = tags) + else CborInt(ULong.MAX_VALUE - value.toULong() + 1uL, Sign.NEGATIVE, tags = tags) /** * Creates an unsigned CBOR integer (major type 0). @@ -120,31 +144,36 @@ public sealed class CborInt( public operator fun invoke( value: ULong, vararg tags: ULong - ): CborInt = CborPositiveInt(value, tags = tags) + ): CborInt = if (value == 0uL) CborInt(value, Sign.ZERO, tags = tags) + else CborInt(value, Sign.POSITIVE, tags = tags) } -} -/** - * Class representing signed CBOR integer (major type 1). - */ -@Serializable(with = CborNegativeIntSerializer::class) -public class CborNegativeInt( - value: Long, - vararg tags: ULong -) : CborInt(tags, value) { - init { - require(value < 0) { "Number must be negative: $value" } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CborInt) return false + if (!super.equals(other)) return false + + if (sign != other.sign) return false + + return true } -} -/** - * Class representing unsigned CBOR integer (major type 0). - */ -@Serializable(with = CborPositiveIntSerializer::class) -public class CborPositiveInt( - value: ULong, - vararg tags: ULong -) : CborInt(tags, value) + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + sign.hashCode() + return result + } + + override fun toString(): String { + return "CborInt(tags=${tags.joinToString(prefix = "[", postfix = "]")}, " + + "value=" + when (sign) { + Sign.POSITIVE, Sign.ZERO -> "" + Sign.NEGATIVE -> "-" + } + + value + + ")" + } +} /** * Class representing CBOR floating point value (major type 7). diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index d466fcf142..f2293c9d03 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -29,8 +29,7 @@ internal object CborElementSerializer : KSerializer, CborSerializer element("CborMap", defer { CborMapSerializer.descriptor }) element("CborList", defer { CborListSerializer.descriptor }) element("CborDouble", defer { CborFloatSerializer.descriptor }) - element("CborInt", defer { CborNegativeIntSerializer.descriptor }) - element("CborUInt", defer { CborPositiveIntSerializer.descriptor }) + element("CborInt", defer { CborIntSerializer.descriptor }) } override fun serialize(encoder: Encoder, value: CborElement) { @@ -65,8 +64,7 @@ internal object CborPrimitiveSerializer : KSerializer>, CborSer is CborBoolean -> encoder.encodeSerializableValue(CborBooleanSerializer, value) is CborByteString -> encoder.encodeSerializableValue(CborByteStringSerializer, value) is CborFloat -> encoder.encodeSerializableValue(CborFloatSerializer, value) - is CborNegativeInt -> encoder.encodeSerializableValue(CborNegativeIntSerializer, value) - is CborPositiveInt -> encoder.encodeSerializableValue(CborPositiveIntSerializer, value) + is CborInt -> encoder.encodeSerializableValue(CborIntSerializer, value) } } @@ -104,56 +102,28 @@ internal object CborNullSerializer : KSerializer, CborSerializer { } -internal object CborIntSerializer : KSerializer>, CborSerializer { +internal object CborIntSerializer : KSerializer, CborSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborInt", PrimitiveKind.LONG) - override fun serialize(encoder: Encoder, value: CborInt<*>) { - when (value) { - is CborNegativeInt -> encoder.encodeSerializableValue(CborNegativeIntSerializer, value) - is CborPositiveInt -> encoder.encodeSerializableValue(CborPositiveIntSerializer, value) + override fun serialize(encoder: Encoder, value: CborInt) { + val cborEncoder = encoder.asCborEncoder() + cborEncoder.encodeTags(value) + when (value.sign) { + //@formatter:off + CborInt.Sign.ZERO, CborInt.Sign.POSITIVE -> cborEncoder.encodePositive(value.value) + CborInt.Sign.NEGATIVE -> cborEncoder.encodeNegative(value.value) + //@formatter:on } } - override fun deserialize(decoder: Decoder): CborInt<*> { + override fun deserialize(decoder: Decoder): CborInt { val result = decoder.asCborDecoder().decodeCborElement() - if (result !is CborInt<*>) throw CborDecodingException("Unexpected CBOR element, expected CborInt, had ${result::class}") + if (result !is CborInt) throw CborDecodingException("Unexpected CBOR element, expected CborInt, had ${result::class}") return result } } -internal object CborNegativeIntSerializer : KSerializer, CborSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborNegativeInt", PrimitiveKind.LONG) - - override fun serialize(encoder: Encoder, value: CborNegativeInt) { - val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value) - encoder.encodeLong(value.value) - } - - override fun deserialize(decoder: Decoder): CborNegativeInt { - decoder.asCborDecoder() - return CborNegativeInt(decoder.decodeLong()) - } -} - -internal object CborPositiveIntSerializer : KSerializer, CborSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborPositiveInt", PrimitiveKind.LONG) - - override fun serialize(encoder: Encoder, value: CborPositiveInt) { - val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value) - encoder.encodeInline(descriptor).encodeSerializableValue(ULong.serializer(), value.value as ULong) - } - - override fun deserialize(decoder: Decoder): CborPositiveInt { - decoder.asCborDecoder() - return CborPositiveInt(decoder.decodeInline(descriptor).decodeSerializableValue(ULong.serializer())) - } -} - internal object CborFloatSerializer : KSerializer, CborSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborDouble", PrimitiveKind.DOUBLE) @@ -323,7 +293,7 @@ private fun defer(deferred: () -> SerialDescriptor): SerialDescriptor = object : private fun CborWriter.encodeTags(value: CborElement) { // Encode tags if present if (value.tags.isNotEmpty()) { - encodeTags(value.tags) + encodeTags(value.tags) } } \ No newline at end of file diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt index d86295e7e3..4ee2a7bc51 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt @@ -27,13 +27,13 @@ internal class CborTreeReader( val result = when (parser.curByte shr 5) { // Get major type from the first 3 bits 0 -> { // Major type 0: unsigned integer - val value = parser.nextNumber() - CborPositiveInt(value.toULong(), tags = tags) + val value = parser.nextULong() + CborInt(value, if (value == 0uL) CborInt.Sign.ZERO else CborInt.Sign.POSITIVE, tags = tags) } 1 -> { // Major type 1: negative integer - val value = parser.nextNumber() - CborNegativeInt(value, tags = tags) + val value = parser.nextULong() + 1uL + CborInt(value, CborInt.Sign.NEGATIVE, tags = tags) } 2 -> { // Major type 2: byte string 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 30584e93c1..d0e40b438d 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -353,12 +353,22 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO return res } - private fun readNumber(): Long { + internal fun nextULong(tags: ULongArray? = null): ULong { + processTags(tags) + val res = readNumber(signed = false) + readByte() + return res.toULong() + } + + private fun readNumber(signed: Boolean = true): Long { val additionalInfo = curByte and 0b000_11111 val negative = (curByte and 0b111_00000) == HEADER_NEGATIVE.toInt() val value = readUnsignedValueFromAdditionalInfo(additionalInfo) - return if (negative) -(value + 1) else value + + return if (signed) { + if (negative) -(value + 1) else value + } else value } private fun ByteArrayInput.readExact(bytes: Int): Long { @@ -667,10 +677,16 @@ internal class StructuredCborParser(internal val element: CborElement, private v override fun nextNumber(tags: ULongArray?): Long { processTags(tags) - return when (layer.current) { - is CborPositiveInt -> (layer.current as CborPositiveInt).value.toLong() - is CborNegativeInt -> (layer.current as CborNegativeInt).value - else -> throw CborDecodingException("Expected number, got ${layer.current::class.simpleName}") + if (layer.current !is CborInt) { + throw CborDecodingException("Expected number, got ${layer.current::class.simpleName}") + } + return (layer.current as CborInt).run { + when (sign) { + //@formatter:off + CborInt.Sign.POSITIVE, CborInt.Sign.ZERO -> value.toLong() + CborInt.Sign.NEGATIVE -> -value.toLong() //possible loss of precision, but inevitable + //@formatter:on + } } } @@ -707,9 +723,8 @@ internal class StructuredCborParser(internal val element: CborElement, private v return when (val key = layer.current) { is CborString -> Triple(key.value, null, tags) - is CborPositiveInt -> Triple(null, key.value.toLong(), tags) - is CborNegativeInt -> Triple(null, key.value, tags) - else -> throw CborDecodingException("Expected string or number key, got ${key?.let { it::class.simpleName } ?: "null"}") + is CborInt -> Triple(null, key.value.toLong(), tags) + else -> throw CborDecodingException("Expected string or number key, got ${key::class.simpleName}") } } 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 040964c98f..70edc5165b 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -99,7 +99,8 @@ internal sealed class CborWriter( override fun encodeLong(value: Long) { getDestination().encodeNumber(value) } - + internal open fun encodeNegative(value: ULong) = getDestination().encodeNegative(value) + internal open fun encodePositive(value: ULong) = getDestination().encodePositive(value) override fun encodeBoolean(value: Boolean) { getDestination().encodeBoolean(value) @@ -346,6 +347,15 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { currentElement += CborInt(value, tags = nextValueTags) } + override fun encodeNegative(value: ULong) { + currentElement += CborInt(value, CborInt.Sign.NEGATIVE, tags = nextValueTags) + } + + override fun encodePositive(value: ULong) { + currentElement += CborInt(value, CborInt.Sign.POSITIVE, tags = nextValueTags) + } + + override fun encodeShort(value: Short) { currentElement += CborInt(value.toLong(), tags = nextValueTags) } @@ -446,6 +456,9 @@ internal fun ByteArrayOutput.encodeBoolean(value: Boolean) = write(if (value) TR internal fun ByteArrayOutput.encodeNumber(value: Long) = write(composeNumber(value)) +internal fun ByteArrayOutput.encodeNegative(value: ULong) = write(composeNegativeULong(value)) +internal fun ByteArrayOutput.encodePositive(value: ULong) = write(composePositive(value)) + internal fun ByteArrayOutput.encodeByteString(data: ByteArray) { this.encodeByteArray(data, HEADER_BYTE_STRING) } @@ -525,3 +538,9 @@ private fun composeNegative(value: Long): ByteArray { data[0] = data[0] or HEADER_NEGATIVE return data } + +private fun composeNegativeULong(value: ULong): ByteArray { + val data = composePositive(value-1uL) + data[0] = data[0] or HEADER_NEGATIVE + return data +} diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborArrayTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborArrayTest.kt index effe128e51..025fd9be2a 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborArrayTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborArrayTest.kt @@ -10,7 +10,7 @@ class CborArrayTest { fun writeReadVerifyArraySize1() { /** * 81 # array(1) - * 26 # negative(6) + * 26 # negative(-7) */ val referenceHexString = "8126" val reference = ClassAs1Array(alg = -7) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt index 40bf07f893..29b9006a02 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt @@ -8,12 +8,14 @@ import kotlin.test.* class CborElementEqualityTest { + //TODO ULONG MIN VALUE TESTS + @Test fun testCborPositiveIntEquality() { - val int1 = CborPositiveInt(42u) - val int2 = CborPositiveInt(42u) - val int3 = CborPositiveInt(43u) - val int4 = CborPositiveInt(42u, 1u) + val int1 = CborInt(42u) + val int2 = CborInt(42u) + val int3 = CborInt(43u) + val int4 = CborInt(42u, 1u) // Same values should be equal assertEquals(int1, int2) @@ -35,18 +37,18 @@ class CborElementEqualityTest { @Test fun testCborNegativeIntEquality() { - val int1 = CborNegativeInt(-42) - val int2 = CborNegativeInt(-42) - val int3 = CborNegativeInt(-43) - val int4 = CborNegativeInt(-42, 1u) + val int1 = CborInt(-42) + val int2 = CborInt(-42) + val int3 = CborInt(-43) + val int4 = CborInt(-42, 1u) assertEquals(int1, int2) assertEquals(int1.hashCode(), int2.hashCode()) assertNotEquals(int1, int3) assertNotEquals(int1, int4) assertNotEquals(int1, null as CborElement?) - assertNotEquals(int1, CborPositiveInt(42u) as CborElement) - assertNotEquals(int1 as CborElement, CborPositiveInt(42u)) + assertNotEquals(int1, CborInt(42u) as CborElement) + assertNotEquals(int1 as CborElement, CborInt(42u)) } @Test @@ -77,8 +79,8 @@ class CborElementEqualityTest { assertNotEquals(string1, string3) assertNotEquals(string1, string4) assertNotEquals(string1, null as CborElement?) - assertNotEquals(string1 as CborElement, CborPositiveInt(123u)) - assertNotEquals(string1, CborPositiveInt(123u) as CborElement) + assertNotEquals(string1 as CborElement, CborInt(123u)) + assertNotEquals(string1, CborInt(123u) as CborElement) } @Test @@ -133,11 +135,11 @@ class CborElementEqualityTest { @Test fun testCborListEquality() { - val list1 = CborList(listOf(CborPositiveInt(1u), CborString("test"))) - val list2 = CborList(listOf(CborPositiveInt(1u), CborString("test"))) - val list3 = CborList(listOf(CborPositiveInt(2u), CborString("test"))) - val list4 = CborList(listOf(CborPositiveInt(1u), CborString("test")), 1u) - val list5 = CborList(listOf(CborPositiveInt(1u))) + val list1 = CborList(listOf(CborInt(1u), CborString("test"))) + val list2 = CborList(listOf(CborInt(1u), CborString("test"))) + val list3 = CborList(listOf(CborInt(2u), CborString("test"))) + val list4 = CborList(listOf(CborInt(1u), CborString("test")), 1u) + val list5 = CborList(listOf(CborInt(1u))) assertEquals(list1, list2) assertEquals(list1.hashCode(), list2.hashCode()) @@ -153,31 +155,31 @@ class CborElementEqualityTest { fun testCborMapEquality() { val map1 = CborMap( mapOf( - CborString("key1") to CborPositiveInt(1u), + CborString("key1") to CborInt(1u), CborString("key2") to CborString("value") ) ) val map2 = CborMap( mapOf( - CborString("key1") to CborPositiveInt(1u), + CborString("key1") to CborInt(1u), CborString("key2") to CborString("value") ) ) val map3 = CborMap( mapOf( - CborString("key1") to CborPositiveInt(2u), + CborString("key1") to CborInt(2u), CborString("key2") to CborString("value") ) ) val map4 = CborMap( mapOf( - CborString("key1") to CborPositiveInt(1u), + CborString("key1") to CborInt(1u), CborString("key2") to CborString("value") ), 1u ) val map5 = CborMap( mapOf( - CborString("key1") to CborPositiveInt(1u) + CborString("key1") to CborInt(1u) ) ) @@ -227,7 +229,7 @@ class CborElementEqualityTest { mapOf( CborString("list") to CborList( listOf( - CborPositiveInt(1u), + CborInt(1u), CborMap(mapOf(CborString("inner") to CborNull())) ) ) @@ -237,7 +239,7 @@ class CborElementEqualityTest { mapOf( CborString("list") to CborList( listOf( - CborPositiveInt(1u), + CborInt(1u), CborMap(mapOf(CborString("inner") to CborNull())) ) ) @@ -247,7 +249,7 @@ class CborElementEqualityTest { mapOf( CborString("list") to CborList( listOf( - CborPositiveInt(2u), + CborInt(2u), CborMap(mapOf(CborString("inner") to CborNull())) ) ) @@ -262,15 +264,15 @@ class CborElementEqualityTest { @Test fun testReflexiveEquality() { val elements = listOf( - CborPositiveInt(42u), - CborNegativeInt(-42), + CborInt(42u), + CborInt(-42), CborFloat(3.14), CborString("test"), CborBoolean(true), CborByteString(byteArrayOf(1, 2, 3)), CborNull(), - CborList(listOf(CborPositiveInt(1u))), - CborMap(mapOf(CborString("key") to CborPositiveInt(1u))) + CborList(listOf(CborInt(1u))), + CborMap(mapOf(CborString("key") to CborInt(1u))) ) elements.forEach { element -> @@ -282,17 +284,17 @@ class CborElementEqualityTest { @Test fun testSymmetricEquality() { val pairs = listOf( - CborPositiveInt(42u) to CborPositiveInt(42u), - CborNegativeInt(-42) to CborNegativeInt(-42), + CborInt(42u) to CborInt(42u), + CborInt(-42) to CborInt(-42), CborFloat(3.14) to CborFloat(3.14), CborString("test") to CborString("test"), CborBoolean(true) to CborBoolean(true), CborByteString(byteArrayOf(1, 2, 3)) to CborByteString(byteArrayOf(1, 2, 3)), CborNull() to CborNull(), - CborList(listOf(CborPositiveInt(1u))) to CborList(listOf(CborPositiveInt(1u))), - CborMap(mapOf(CborString("key") to CborPositiveInt(1u))) to CborMap( + CborList(listOf(CborInt(1u))) to CborList(listOf(CborInt(1u))), + CborMap(mapOf(CborString("key") to CborInt(1u))) to CborMap( mapOf( - CborString("key") to CborPositiveInt( + CborString("key") to CborInt( 1u ) ) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index 102c5f911e..f4aee202e5 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -22,11 +22,93 @@ class CborElementTest { @Test fun testCborNumber() { - val numberElement = CborPositiveInt(42u) + val numberElement = CborInt(42u) val numberBytes = cbor.encodeToByteArray(numberElement) val decodedNumber = cbor.decodeFromByteArray(numberBytes) assertEquals(numberElement, decodedNumber) - assertEquals(42u, (decodedNumber as CborPositiveInt).value) + assertEquals(42u, (decodedNumber as CborInt).value) + } + + @Test + fun testCborNumberZero() { + val numberElement = CborInt(0uL) + assertEquals(numberElement, CborInt(0)) + assertEquals(numberElement.sign, CborInt.Sign.ZERO) + assertEquals(numberElement.value, 0uL) + val numberBytes = cbor.encodeToByteArray(numberElement) + val decodedNumber = cbor.decodeFromByteArray(numberBytes) + assertEquals(numberElement, decodedNumber) + assertEquals(0uL, (decodedNumber as CborInt).value) + } + + @Test + fun testCborNumberMax() { + val numberElement = CborInt(ULong.MAX_VALUE) + assertEquals(numberElement.sign, CborInt.Sign.POSITIVE) + assertEquals(numberElement.value, ULong.MAX_VALUE) + val numberBytes = cbor.encodeToByteArray(numberElement) + val decodedNumber = cbor.decodeFromByteArray(numberBytes) + assertEquals(numberElement, decodedNumber) + assertEquals(ULong.MAX_VALUE, (decodedNumber as CborInt).value) + } + + @Test + fun testCborNumberMaxHalv() { + val numberElement = CborInt(Long.MAX_VALUE) + assertEquals(numberElement.sign, CborInt.Sign.POSITIVE) + assertEquals(numberElement.value, Long.MAX_VALUE.toULong()) + val numberBytes = cbor.encodeToByteArray(numberElement) + val decodedNumber = cbor.decodeFromByteArray(numberBytes) + assertEquals(numberElement, decodedNumber) + assertEquals(Long.MAX_VALUE.toULong(), (decodedNumber as CborInt).value) + } + + + @Test + fun testCborNumberMin() { + val numberElement = CborInt(ULong.MAX_VALUE, sign = CborInt.Sign.NEGATIVE) + assertEquals(numberElement.sign, CborInt.Sign.NEGATIVE) + assertEquals(numberElement.value, ULong.MAX_VALUE) + val numberBytes = cbor.encodeToByteArray(numberElement) + val decodedNumber = cbor.decodeFromByteArray(numberBytes) + assertEquals(numberElement, decodedNumber) + assertEquals(ULong.MAX_VALUE, (decodedNumber as CborInt).value) + + val lossOfPrecision = cbor.decodeFromCborElement(numberElement) + + assertEquals(1L, lossOfPrecision) + assertEquals(1L, numberElement.toLong()) + } + + + @Test + fun testCborNumberMinHalv() { + val numberElement = CborInt(Long.MAX_VALUE.toULong(), sign = CborInt.Sign.NEGATIVE) + assertEquals(numberElement.sign, CborInt.Sign.NEGATIVE) + assertEquals(numberElement.value, Long.MAX_VALUE.toULong()) + val numberBytes = cbor.encodeToByteArray(numberElement) + val decodedNumber = cbor.decodeFromByteArray(numberBytes) + assertEquals(numberElement, decodedNumber) + assertEquals(Long.MAX_VALUE.toULong(), (decodedNumber as CborInt).value) + + val long = cbor.decodeFromCborElement(numberElement) + + assertEquals(Long.MIN_VALUE+1, long) + assertEquals(Long.MIN_VALUE+1, numberElement.toLong()) + } + + + + @Test + fun testCborNumberLong() { + assertEquals(Long.MAX_VALUE, CborInt(Long.MAX_VALUE).toLong()) + assertEquals(Long.MIN_VALUE, CborInt(Long.MIN_VALUE).toLong()) + } + + @Test + fun testCborNumberIllegal() { + assertFails { CborInt(ULong.MAX_VALUE, sign = CborInt.Sign.ZERO) } + assertFails { CborInt(1uL, sign = CborInt.Sign.ZERO) } } @Test @@ -61,7 +143,7 @@ class CborElementTest { fun testCborList() { val listElement = CborList( listOf( - CborPositiveInt(1u), + CborInt(1u), CborString("two"), CborBoolean(true), CborNull() @@ -75,8 +157,8 @@ class CborElementTest { assertEquals(4, decodedList.size) // Verify individual elements - assertTrue(decodedList[0] is CborPositiveInt) - assertEquals(1u, (decodedList[0] as CborPositiveInt).value) + assertTrue(decodedList[0] is CborInt) + assertEquals(1u, (decodedList[0] as CborInt).value) assertTrue(decodedList[1] is CborString) assertEquals("two", (decodedList[1] as CborString).value) @@ -91,9 +173,9 @@ class CborElementTest { fun testCborMap() { val mapElement = CborMap( mapOf( - CborString("key1") to CborPositiveInt(42u), + CborString("key1") to CborInt(42u), CborString("key2") to CborString("value"), - CborPositiveInt(3u) to CborBoolean(true), + CborInt(3u) to CborBoolean(true), CborNull() to CborNull() ) ) @@ -107,16 +189,16 @@ class CborElementTest { // Verify individual entries assertTrue(decodedMap.containsKey(CborString("key1"))) val value1 = decodedMap[CborString("key1")] - assertTrue(value1 is CborPositiveInt) - assertEquals(42u, (value1 as CborPositiveInt).value) + assertTrue(value1 is CborInt) + assertEquals(42u, (value1 as CborInt).value) assertTrue(decodedMap.containsKey(CborString("key2"))) val value2 = decodedMap[CborString("key2")] assertTrue(value2 is CborString) assertEquals("value", (value2 as CborString).value) - assertTrue(decodedMap.containsKey(CborPositiveInt(3u))) - val value3 = decodedMap[CborPositiveInt(3u)] + assertTrue(decodedMap.containsKey(CborInt(3u))) + val value3 = decodedMap[CborInt(3u)] assertTrue(value3 is CborBoolean) assertEquals(true, (value3 as CborBoolean).value) @@ -132,7 +214,7 @@ class CborElementTest { mapOf( CborString("primitives") to CborList( listOf( - CborPositiveInt(123u), + CborInt(123u), CborString("text"), CborBoolean(false), CborByteString(byteArrayOf(10, 20, 30)), @@ -143,8 +225,8 @@ class CborElementTest { mapOf( CborString("inner") to CborList( listOf( - CborPositiveInt(1u), - CborPositiveInt(2u) + CborInt(1u), + CborInt(2u) ) ), CborString("empty") to CborList(emptyList()) @@ -166,8 +248,8 @@ class CborElementTest { assertEquals(5, primitivesValue.size) - assertTrue(primitivesValue[0] is CborPositiveInt) - assertEquals(123u, (primitivesValue[0] as CborPositiveInt).value) + assertTrue(primitivesValue[0] is CborInt) + assertEquals(123u, (primitivesValue[0] as CborInt).value) assertTrue(primitivesValue[1] is CborString) assertEquals("text", (primitivesValue[1] as CborString).value) @@ -194,11 +276,11 @@ class CborElementTest { assertEquals(2, innerValue.size) - assertTrue(innerValue[0] is CborPositiveInt) - assertEquals(1u, (innerValue[0] as CborPositiveInt).value) + assertTrue(innerValue[0] is CborInt) + assertEquals(1u, (innerValue[0] as CborInt).value) - assertTrue(innerValue[1] is CborPositiveInt) - assertEquals(2u, (innerValue[1] as CborPositiveInt).value) + assertTrue(innerValue[1] is CborInt) + assertEquals(2u, (innerValue[1] as CborInt).value) // Verify the empty list assertTrue(nestedValue.containsKey(CborString("empty"))) @@ -212,7 +294,7 @@ class CborElementTest { @Test fun testDecodePositiveInt() { // Test data from CborParserTest.testParseIntegers - val element = cbor.decodeFromHexString("0C") as CborPositiveInt + val element = cbor.decodeFromHexString("0C") as CborInt assertEquals(12u, element.value) } @@ -258,9 +340,9 @@ class CborElementTest { assertTrue(element is CborList) val list = element as CborList assertEquals(3, list.size) - assertEquals(1u, (list[0] as CborPositiveInt).value) - assertEquals(255u, (list[1] as CborPositiveInt).value) - assertEquals(65536u, (list[2] as CborPositiveInt).value) + assertEquals(1u, (list[0] as CborInt).value) + assertEquals(255u, (list[1] as CborInt).value) + assertEquals(65536u, (list[2] as CborInt).value) } @Test @@ -300,9 +382,9 @@ class CborElementTest { // Check the nested map val nestedMap = map[CborString("d")] as CborMap assertEquals(3, nestedMap.size) - assertEquals(CborPositiveInt(1u), nestedMap[CborString("1")]) - assertEquals(CborPositiveInt(2u), nestedMap[CborString("2")]) - assertEquals(CborPositiveInt(3u), nestedMap[CborString("3")]) + assertEquals(CborInt(1u), nestedMap[CborString("1")]) + assertEquals(CborInt(2u), nestedMap[CborString("2")]) + assertEquals(CborInt(3u), nestedMap[CborString("3")]) } @OptIn(ExperimentalStdlibApi::class) @@ -337,7 +419,7 @@ class CborElementTest { str = "A string, is a string, is a string", bStr = CborByteString(byteArrayOf()), cborElement = CborBoolean(false), - cborPositiveInt = CborPositiveInt(1u), + cborPositiveInt = CborInt(1u), cborInt = CborInt(-1), tagged = 26 ), @@ -364,7 +446,7 @@ class CborElementTest { str = "A string, is a string, is a string", bStr = null, cborElement = CborBoolean(false), - cborPositiveInt = CborPositiveInt(1u), + cborPositiveInt = CborInt(1u), cborInt = CborInt(-1), tagged = 26 ), @@ -392,7 +474,7 @@ class CborElementTest { str = "A string, is a string, is a string", bStr = null, cborElement = CborMap(mapOf(CborByteString(byteArrayOf(1, 3, 3, 7)) to CborNull())), - cborPositiveInt = CborPositiveInt(1u), + cborPositiveInt = CborInt(1u), cborInt = CborInt(-1), tagged = 26 ), @@ -421,7 +503,7 @@ class CborElementTest { str = "A string, is a string, is a string", bStr = null, cborElement = CborNull(), - cborPositiveInt = CborPositiveInt(1u), + cborPositiveInt = CborInt(1u), cborInt = CborInt(-1), tagged = 26 ), @@ -449,7 +531,7 @@ class CborElementTest { str = "A string, is a string, is a string", bStr = CborByteString(byteArrayOf(), 1u, 3u), cborElement = CborBoolean(false), - cborPositiveInt = CborPositiveInt(1u), + cborPositiveInt = CborInt(1u), cborInt = CborInt(-1), tagged = 26 ), @@ -476,7 +558,7 @@ class CborElementTest { str = "A string, is a string, is a string", bStr = CborByteString(byteArrayOf(), 1u, 3u), cborElement = CborBoolean(false), - cborPositiveInt = CborPositiveInt(1u), + cborPositiveInt = CborInt(1u), cborInt = CborInt(-1), tagged = 26 ), @@ -617,7 +699,7 @@ data class MixedBag( val bStr: CborByteString?, val cborElement: CborElement?, val cborPositiveInt: CborPrimitive<*>, - val cborInt: CborInt<*>, + val cborInt: CborInt, @KeyTags(42u) @ValueTags(2337u) val tagged: Int From 42940342265b96e4ffa24a1d4585ae6f5696edba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Wed, 27 Aug 2025 16:53:12 +0200 Subject: [PATCH 27/68] add CborEncoder.encoderCborElement --- .../cbor/api/kotlinx-serialization-cbor.api | 2 ++ .../api/kotlinx-serialization-cbor.klib.api | 2 ++ .../kotlinx/serialization/cbor/CborElement.kt | 10 +++++++ .../kotlinx/serialization/cbor/CborEncoder.kt | 30 +++++++++++++++++++ .../serialization/cbor/CborElementTest.kt | 5 ++++ 5 files changed, 49 insertions(+) diff --git a/formats/cbor/api/kotlinx-serialization-cbor.api b/formats/cbor/api/kotlinx-serialization-cbor.api index 1a638aab4c..b5824d1c64 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.api @@ -114,11 +114,13 @@ public final class kotlinx/serialization/cbor/CborElement$Companion { } public abstract interface class kotlinx/serialization/cbor/CborEncoder : kotlinx/serialization/encoding/Encoder { + public fun encodeCborElement (Lkotlinx/serialization/cbor/CborElement;)V public abstract fun getCbor ()Lkotlinx/serialization/cbor/Cbor; } public final class kotlinx/serialization/cbor/CborEncoder$DefaultImpls { public static fun beginCollection (Lkotlinx/serialization/cbor/CborEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;I)Lkotlinx/serialization/encoding/CompositeEncoder; + public static fun encodeCborElement (Lkotlinx/serialization/cbor/CborEncoder;Lkotlinx/serialization/cbor/CborElement;)V public static fun encodeNotNullMark (Lkotlinx/serialization/cbor/CborEncoder;)V public static fun encodeNullableSerializableValue (Lkotlinx/serialization/cbor/CborEncoder;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)V public static fun encodeSerializableValue (Lkotlinx/serialization/cbor/CborEncoder;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)V diff --git a/formats/cbor/api/kotlinx-serialization-cbor.klib.api b/formats/cbor/api/kotlinx-serialization-cbor.klib.api index ae73a9cc79..2c5b76c4cf 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.klib.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.klib.api @@ -52,6 +52,8 @@ abstract interface kotlinx.serialization.cbor/CborDecoder : kotlinx.serializatio abstract interface kotlinx.serialization.cbor/CborEncoder : kotlinx.serialization.encoding/Encoder { // kotlinx.serialization.cbor/CborEncoder|null[0] abstract val cbor // kotlinx.serialization.cbor/CborEncoder.cbor|{}cbor[0] abstract fun (): kotlinx.serialization.cbor/Cbor // kotlinx.serialization.cbor/CborEncoder.cbor.|(){}[0] + + open fun encodeCborElement(kotlinx.serialization.cbor/CborElement) // kotlinx.serialization.cbor/CborEncoder.encodeCborElement|encodeCborElement(kotlinx.serialization.cbor.CborElement){}[0] } final class kotlinx.serialization.cbor/CborBoolean : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborBoolean|null[0] diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt index 2d815d6061..31d10b92e9 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -21,6 +21,7 @@ import kotlinx.serialization.cbor.internal.* * The whole hierarchy is [serializable][Serializable] only by [Cbor] format. */ @Serializable(with = CborElementSerializer::class) +@ExperimentalSerializationApi public sealed class CborElement( /** * CBOR tags associated with this element. @@ -60,6 +61,7 @@ public sealed class CborElement( * CBOR primitives include numbers, strings, booleans, byte arrays and special null value [CborNull]. */ @Serializable(with = CborPrimitiveSerializer::class) +@ExperimentalSerializationApi public sealed class CborPrimitive( public val value: T, tags: ULongArray = ulongArrayOf() @@ -96,6 +98,7 @@ public sealed class CborPrimitive( * depending on the value of [sign]. Note that [absoluteValue] **must** be `0` when sign is set to [Sign.ZERO] */ @Serializable(with = CborIntSerializer::class) +@ExperimentalSerializationApi public class CborInt( absoluteValue: ULong, public val sign: Sign, @@ -179,6 +182,7 @@ public class CborInt( * Class representing CBOR floating point value (major type 7). */ @Serializable(with = CborFloatSerializer::class) +@ExperimentalSerializationApi public class CborFloat( value: Double, vararg tags: ULong @@ -188,6 +192,7 @@ public class CborFloat( * Class representing CBOR string value. */ @Serializable(with = CborStringSerializer::class) +@ExperimentalSerializationApi public class CborString( value: String, vararg tags: ULong @@ -197,6 +202,7 @@ public class CborString( * Class representing CBOR boolean value. */ @Serializable(with = CborBooleanSerializer::class) +@ExperimentalSerializationApi public class CborBoolean( value: Boolean, vararg tags: ULong @@ -206,6 +212,7 @@ public class CborBoolean( * Class representing CBOR byte string value. */ @Serializable(with = CborByteStringSerializer::class) +@ExperimentalSerializationApi public class CborByteString( value: ByteArray, vararg tags: ULong @@ -235,6 +242,7 @@ public class CborByteString( * Class representing CBOR `null` value */ @Serializable(with = CborNullSerializer::class) +@ExperimentalSerializationApi public class CborNull(vararg tags: ULong) : CborPrimitive(Unit, tags) /** @@ -244,6 +252,7 @@ public class CborNull(vararg tags: ULong) : CborPrimitive(Unit, tags) * traditional methods like [Map.get] or [Map.getValue] to obtain CBOR elements. */ @Serializable(with = CborMapSerializer::class) +@ExperimentalSerializationApi public class CborMap( private val content: Map, vararg tags: ULong @@ -270,6 +279,7 @@ public class CborMap( * traditional methods like [List.get] or [List.size] to obtain CBOR elements. */ @Serializable(with = CborListSerializer::class) +@ExperimentalSerializationApi public class CborList( private val content: List, vararg tags: ULong diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt index 7cfead426a..e5e03ee43f 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt @@ -5,6 +5,7 @@ package kotlinx.serialization.cbor import kotlinx.serialization.* +import kotlinx.serialization.cbor.internal.CborElementSerializer import kotlinx.serialization.encoding.* /** @@ -31,4 +32,33 @@ public interface CborEncoder : Encoder { * Exposes the current [Cbor] instance and all its configuration flags. Useful for low-level custom serializers. */ public val cbor: Cbor + /** + * Appends the given CBOR [element] to the current output. + * This method is allowed to invoke only as the part of the whole serialization process of the class, + * calling this method after invoking [beginStructure] or any `encode*` method will lead to unspecified behaviour + * and may produce an invalid CBOR result. + * For example: + * ``` + * class Holder(val value: Int, val list: List()) + * + * // Holder serialize method + * fun serialize(encoder: Encoder, value: Holder) { + * // Completely okay, the whole Holder object is read + * val cborObject = CborMap(...) // build a CborMap from Holder + * (encoder as CborEncoder).encodeCborElement(cborObject) // Write it + * } + * + * // Incorrect Holder serialize method + * fun serialize(encoder: Encoder, value: Holder) { + * val composite = encoder.beginStructure(descriptor) + * composite.encodeSerializableElement(descriptor, 0, Int.serializer(), value.value) + * val array = CborList(value.list.map { CborInt(it.toLong()) }) + * // Incorrect, encoder is already in an intermediate state after encodeSerializableElement + * (composite as CborEncoder).encodeCborElement(array) + * composite.endStructure(descriptor) + * // ... + * } + * ``` + */ + public fun encodeCborElement(element: CborElement): Unit = encodeSerializableValue(CborElementSerializer, element) } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index f4aee202e5..32fcfdb16f 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -180,6 +180,11 @@ class CborElementTest { ) ) val mapBytes = cbor.encodeToByteArray(mapElement) + + val output = ByteArrayOutput() + IndefiniteLengthCborWriter(cbor, output).encodeCborElement(mapElement) + assertEquals(mapBytes.toHexString(),output.toByteArray().toHexString() ) + val decodedMap = cbor.decodeFromByteArray(mapBytes) // Verify the type and size From 0cb58c937cd76b3872c20b1caa3c7b9ba6f70588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Wed, 27 Aug 2025 17:07:54 +0200 Subject: [PATCH 28/68] pimp CborEncoder --- .../cbor/api/kotlinx-serialization-cbor.api | 3 +++ .../api/kotlinx-serialization-cbor.klib.api | 3 +++ .../kotlinx/serialization/cbor/CborEncoder.kt | 17 +++++++++++++++ .../cbor/internal/CborElementSerializers.kt | 21 ++++++++++--------- .../serialization/cbor/internal/Encoder.kt | 6 ++---- 5 files changed, 36 insertions(+), 14 deletions(-) diff --git a/formats/cbor/api/kotlinx-serialization-cbor.api b/formats/cbor/api/kotlinx-serialization-cbor.api index b5824d1c64..023bfe5b59 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.api @@ -115,6 +115,9 @@ public final class kotlinx/serialization/cbor/CborElement$Companion { public abstract interface class kotlinx/serialization/cbor/CborEncoder : kotlinx/serialization/encoding/Encoder { public fun encodeCborElement (Lkotlinx/serialization/cbor/CborElement;)V + public abstract fun encodeNegative-VKZWuLQ (J)V + public abstract fun encodePositive-VKZWuLQ (J)V + public abstract fun encodeTags-QwZRm1k ([J)V public abstract fun getCbor ()Lkotlinx/serialization/cbor/Cbor; } diff --git a/formats/cbor/api/kotlinx-serialization-cbor.klib.api b/formats/cbor/api/kotlinx-serialization-cbor.klib.api index 2c5b76c4cf..0d0319aaff 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.klib.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.klib.api @@ -53,6 +53,9 @@ abstract interface kotlinx.serialization.cbor/CborEncoder : kotlinx.serializatio abstract val cbor // kotlinx.serialization.cbor/CborEncoder.cbor|{}cbor[0] abstract fun (): kotlinx.serialization.cbor/Cbor // kotlinx.serialization.cbor/CborEncoder.cbor.|(){}[0] + abstract fun encodeNegative(kotlin/ULong) // kotlinx.serialization.cbor/CborEncoder.encodeNegative|encodeNegative(kotlin.ULong){}[0] + abstract fun encodePositive(kotlin/ULong) // kotlinx.serialization.cbor/CborEncoder.encodePositive|encodePositive(kotlin.ULong){}[0] + abstract fun encodeTags(kotlin/ULongArray) // kotlinx.serialization.cbor/CborEncoder.encodeTags|encodeTags(kotlin.ULongArray){}[0] open fun encodeCborElement(kotlinx.serialization.cbor/CborElement) // kotlinx.serialization.cbor/CborEncoder.encodeCborElement|encodeCborElement(kotlinx.serialization.cbor.CborElement){}[0] } diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt index e5e03ee43f..308ae48682 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt @@ -6,6 +6,8 @@ package kotlinx.serialization.cbor import kotlinx.serialization.* import kotlinx.serialization.cbor.internal.CborElementSerializer +import kotlinx.serialization.cbor.internal.encodeNegative +import kotlinx.serialization.cbor.internal.encodePositive import kotlinx.serialization.encoding.* /** @@ -61,4 +63,19 @@ public interface CborEncoder : Encoder { * ``` */ public fun encodeCborElement(element: CborElement): Unit = encodeSerializableValue(CborElementSerializer, element) + + /** + * Allows manually encoding CBOR tags. Use with caution, as it is possible to produce invalid CBOR if invoked carelessly! + */ + public fun encodeTags(@OptIn(kotlin.ExperimentalUnsignedTypes::class) tags: ULongArray): Unit + + /** + * Encode a negative value as [CborInt]. This function exists to encode negative values exceeding [Long.MIN_VALUE] + */ + public fun encodeNegative(value: ULong) + + /** + * Encode a positive value as [CborInt]. This function exists to encode negative values exceeding [Long.MAX_VALUE] + */ + public fun encodePositive(value: ULong) } diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index f2293c9d03..9591e86d5d 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -86,7 +86,7 @@ internal object CborNullSerializer : KSerializer, CborSerializer { override fun serialize(encoder: Encoder, value: CborNull) { val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value) + cborEncoder.encodeTags(value.tags) encoder.encodeNull() } @@ -108,7 +108,7 @@ internal object CborIntSerializer : KSerializer, CborSerializer { override fun serialize(encoder: Encoder, value: CborInt) { val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value) + cborEncoder.encodeTags(value.tags) when (value.sign) { //@formatter:off CborInt.Sign.ZERO, CborInt.Sign.POSITIVE -> cborEncoder.encodePositive(value.value) @@ -130,7 +130,7 @@ internal object CborFloatSerializer : KSerializer, CborSerializer { override fun serialize(encoder: Encoder, value: CborFloat) { val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value) + cborEncoder.encodeTags(value.tags) encoder.encodeDouble(value.value) } @@ -150,7 +150,7 @@ internal object CborStringSerializer : KSerializer, CborSerializer { override fun serialize(encoder: Encoder, value: CborString) { val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value) + cborEncoder.encodeTags(value.tags) encoder.encodeString(value.value) } @@ -172,7 +172,7 @@ internal object CborBooleanSerializer : KSerializer, CborSerializer override fun serialize(encoder: Encoder, value: CborBoolean) { val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value) + cborEncoder.encodeTags(value.tags) encoder.encodeBoolean(value.value) } @@ -194,8 +194,9 @@ internal object CborByteStringSerializer : KSerializer, CborSeri override fun serialize(encoder: Encoder, value: CborByteString) { val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value) - cborEncoder.encodeByteString(value.value) + cborEncoder.encodeTags(value.tags) + //this we really don't want to expose so we cast here + (cborEncoder as CborWriter).encodeByteString(value.value) } override fun deserialize(decoder: Decoder): CborByteString { @@ -221,7 +222,7 @@ internal object CborMapSerializer : KSerializer, CborSerializer { override fun serialize(encoder: Encoder, value: CborMap) { val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value) + cborEncoder.encodeTags(value.tags) MapSerializer(CborElementSerializer, CborElementSerializer).serialize(encoder, value) } @@ -245,7 +246,7 @@ internal object CborListSerializer : KSerializer, CborSerializer { override fun serialize(encoder: Encoder, value: CborList) { val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value) + cborEncoder.encodeTags(value.tags) ListSerializer(CborElementSerializer).serialize(encoder, value) } @@ -263,7 +264,7 @@ internal fun Decoder.asCborDecoder(): CborDecoder = this as? CborDecoder ) /*need to expose writer to access encodeTag()*/ -internal fun Encoder.asCborEncoder() = this as? CborWriter +internal fun Encoder.asCborEncoder() = this as? CborEncoder ?: throw IllegalStateException( "This serializer can be used only with Cbor format." + "Expected Encoder to be CborEncoder, got ${this::class}" 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 70edc5165b..c66947f678 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -99,8 +99,8 @@ internal sealed class CborWriter( override fun encodeLong(value: Long) { getDestination().encodeNumber(value) } - internal open fun encodeNegative(value: ULong) = getDestination().encodeNegative(value) - internal open fun encodePositive(value: ULong) = getDestination().encodePositive(value) + override fun encodeNegative(value: ULong) = getDestination().encodeNegative(value) + override fun encodePositive(value: ULong) = getDestination().encodePositive(value) override fun encodeBoolean(value: Boolean) { getDestination().encodeBoolean(value) @@ -148,8 +148,6 @@ internal sealed class CborWriter( incrementChildren() // needed for definite len encoding, NOOP for indefinite length encoding return true } - - internal abstract fun encodeTags(tags: ULongArray) } From 46f8b464da2e3dfe9643f777ca0e610a1456b3fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Thu, 4 Dec 2025 07:06:27 +0100 Subject: [PATCH 29/68] Apply suggestions from code review Co-authored-by: Filipp Zhinkin --- docs/formats.md | 6 +++--- .../serialization/cbor/internal/CborElementSerializers.kt | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/formats.md b/docs/formats.md index 76260daa1e..d2b6a9ed27 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -409,10 +409,10 @@ of a generic serializable class with `@ValueTags`. A [CborElement] class has three direct subtypes, closely following CBOR grammar: -* [CborPrimitive] represents primitive CBOR elements, such as string, integer, float boolean, and null. - CBOR byte strings are also treated as primitives +* [CborPrimitive] represents primitive CBOR elements, such as string, integer, float, boolean, and null. + CBOR byte strings are also treated as primitives. Each primitive has a [value][CborPrimitive.value]. Depending on the concrete type of the primitive, it maps - to corresponding Kotlin Types such as `String`, `Int`, `Double`, etc. + to corresponding Kotlin Types such as `String`, `Long`, `Double`, etc. Note that Cbor discriminates between positive ("unsigned") and negative ("signed") integers! `CborPrimitive` is itself an umbrella type (a sealed class) for the following concrete primitives: * [CborNull] mapping to a Kotlin `null` diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index 9591e86d5d..069dc9fa8a 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -259,14 +259,14 @@ internal object CborListSerializer : KSerializer, CborSerializer { internal fun Decoder.asCborDecoder(): CborDecoder = this as? CborDecoder ?: throw IllegalStateException( - "This serializer can be used only with Cbor format." + + "This serializer can be used only with Cbor format. " + "Expected Decoder to be CborDecoder, got ${this::class}" ) /*need to expose writer to access encodeTag()*/ internal fun Encoder.asCborEncoder() = this as? CborEncoder ?: throw IllegalStateException( - "This serializer can be used only with Cbor format." + + "This serializer can be used only with Cbor format. " + "Expected Encoder to be CborEncoder, got ${this::class}" ) From 1931b109051a06df3ff2d2a18c560afc1fbf19e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 15:52:45 +0100 Subject: [PATCH 30/68] cborInt sign refactor --- .../kotlinx/serialization/cbor/CborElement.kt | 40 +++++++++---------- .../cbor/internal/CborElementSerializers.kt | 6 +-- .../cbor/internal/CborTreeReader.kt | 4 +- .../serialization/cbor/internal/Decoder.kt | 6 +-- .../serialization/cbor/internal/Encoder.kt | 4 +- .../serialization/cbor/CborElementTest.kt | 24 +++++------ 6 files changed, 37 insertions(+), 47 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt index 31d10b92e9..2d9b3ca226 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -95,32 +95,26 @@ public sealed class CborPrimitive( * * signed CBOR integer (major type 1 encompassing `-2^64..-1`) * * unsigned CBOR integer (major type 0 encompassing `0..2^64-1`) * - * depending on the value of [sign]. Note that [absoluteValue] **must** be `0` when sign is set to [Sign.ZERO] + * depending on the value of [isPositive]. Note that [absoluteValue] **must not be** `0` when [isPositive] is set to `false`. */ @Serializable(with = CborIntSerializer::class) @ExperimentalSerializationApi public class CborInt( absoluteValue: ULong, - public val sign: Sign, + public val isPositive: Boolean, vararg tags: ULong ) : CborPrimitive(absoluteValue, tags) { init { - if (sign == Sign.ZERO) require(absoluteValue == 0uL) { "Illegal absolute value $absoluteValue for Sign.ZERO" } - } - - public enum class Sign { - POSITIVE, - NEGATIVE, - ZERO + if (!isPositive) require(absoluteValue > 0uL) { "Illegal absolute value $absoluteValue for a negative number." } } /** * **WARNING! Possible truncation/overflow!** E.g., `-2^64` -> `1` */ - public fun toLong(): Long = when (sign) { - Sign.POSITIVE, Sign.ZERO -> value.toLong() - Sign.NEGATIVE -> -value.toLong() + public fun toLong(): Long = when (isPositive) { + isPositive -> value.toLong() + else -> -value.toLong() } @@ -137,9 +131,8 @@ public class CborInt( value: Long, vararg tags: ULong ): CborInt = - if (value == 0L) CborInt(value.toULong(), Sign.ZERO, tags = tags) - else if (value > 0L) CborInt(value.toULong(), Sign.POSITIVE, tags = tags) - else CborInt(ULong.MAX_VALUE - value.toULong() + 1uL, Sign.NEGATIVE, tags = tags) + if (value >= 0L) CborInt(value.toULong(), isPositive = true, tags = tags) + else CborInt(ULong.MAX_VALUE - value.toULong() + 1uL, isPositive = false, tags = tags) /** * Creates an unsigned CBOR integer (major type 0). @@ -147,8 +140,7 @@ public class CborInt( public operator fun invoke( value: ULong, vararg tags: ULong - ): CborInt = if (value == 0uL) CborInt(value, Sign.ZERO, tags = tags) - else CborInt(value, Sign.POSITIVE, tags = tags) + ): CborInt = CborInt(value, isPositive = true, tags = tags) } override fun equals(other: Any?): Boolean { @@ -156,22 +148,22 @@ public class CborInt( if (other !is CborInt) return false if (!super.equals(other)) return false - if (sign != other.sign) return false + if (isPositive != other.isPositive) return false return true } override fun hashCode(): Int { var result = super.hashCode() - result = 31 * result + sign.hashCode() + result = 31 * result + isPositive.hashCode() return result } override fun toString(): String { return "CborInt(tags=${tags.joinToString(prefix = "[", postfix = "]")}, " + - "value=" + when (sign) { - Sign.POSITIVE, Sign.ZERO -> "" - Sign.NEGATIVE -> "-" + "value=" + when (isPositive) { + true -> "" + false -> "-" } + value + ")" @@ -270,6 +262,10 @@ public class CborMap( ")" } + public operator fun get(key: String?): CborElement? = content[key?.let { CborString(it) }?:CborNull() ] + public operator fun get(key: Long?): CborElement? = content[key?.let { CborInt(it) }?:CborNull() ] + public operator fun get(key: Int?): CborElement? = content[key?.let { CborInt(it.toULong()) }?:CborNull() ] + } /** diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index 069dc9fa8a..9a70c36396 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -109,10 +109,10 @@ internal object CborIntSerializer : KSerializer, CborSerializer { override fun serialize(encoder: Encoder, value: CborInt) { val cborEncoder = encoder.asCborEncoder() cborEncoder.encodeTags(value.tags) - when (value.sign) { + when (value.isPositive) { //@formatter:off - CborInt.Sign.ZERO, CborInt.Sign.POSITIVE -> cborEncoder.encodePositive(value.value) - CborInt.Sign.NEGATIVE -> cborEncoder.encodeNegative(value.value) + true -> cborEncoder.encodePositive(value.value) + false -> cborEncoder.encodeNegative(value.value) //@formatter:on } } diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt index 4ee2a7bc51..c25a2c4f66 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt @@ -28,12 +28,12 @@ internal class CborTreeReader( val result = when (parser.curByte shr 5) { // Get major type from the first 3 bits 0 -> { // Major type 0: unsigned integer val value = parser.nextULong() - CborInt(value, if (value == 0uL) CborInt.Sign.ZERO else CborInt.Sign.POSITIVE, tags = tags) + CborInt(value, isPositive = true, tags = tags) } 1 -> { // Major type 1: negative integer val value = parser.nextULong() + 1uL - CborInt(value, CborInt.Sign.NEGATIVE, tags = tags) + CborInt(value, isPositive = false, tags = tags) } 2 -> { // Major type 2: byte string 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 d0e40b438d..a5f539a8d2 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -681,10 +681,10 @@ internal class StructuredCborParser(internal val element: CborElement, private v throw CborDecodingException("Expected number, got ${layer.current::class.simpleName}") } return (layer.current as CborInt).run { - when (sign) { + when (isPositive) { //@formatter:off - CborInt.Sign.POSITIVE, CborInt.Sign.ZERO -> value.toLong() - CborInt.Sign.NEGATIVE -> -value.toLong() //possible loss of precision, but inevitable + true -> value.toLong() + false -> -value.toLong() //possible loss of precision, but inevitable //@formatter:on } } 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 c66947f678..c49101c515 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -346,11 +346,11 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { } override fun encodeNegative(value: ULong) { - currentElement += CborInt(value, CborInt.Sign.NEGATIVE, tags = nextValueTags) + currentElement += CborInt(value, isPositive = false, tags = nextValueTags) } override fun encodePositive(value: ULong) { - currentElement += CborInt(value, CborInt.Sign.POSITIVE, tags = nextValueTags) + currentElement += CborInt(value, isPositive = true, tags = nextValueTags) } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index 32fcfdb16f..45f00015c7 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -33,7 +33,7 @@ class CborElementTest { fun testCborNumberZero() { val numberElement = CborInt(0uL) assertEquals(numberElement, CborInt(0)) - assertEquals(numberElement.sign, CborInt.Sign.ZERO) + assertEquals(numberElement.isPositive, true) assertEquals(numberElement.value, 0uL) val numberBytes = cbor.encodeToByteArray(numberElement) val decodedNumber = cbor.decodeFromByteArray(numberBytes) @@ -44,7 +44,7 @@ class CborElementTest { @Test fun testCborNumberMax() { val numberElement = CborInt(ULong.MAX_VALUE) - assertEquals(numberElement.sign, CborInt.Sign.POSITIVE) + assertEquals(numberElement.isPositive, true) assertEquals(numberElement.value, ULong.MAX_VALUE) val numberBytes = cbor.encodeToByteArray(numberElement) val decodedNumber = cbor.decodeFromByteArray(numberBytes) @@ -55,7 +55,7 @@ class CborElementTest { @Test fun testCborNumberMaxHalv() { val numberElement = CborInt(Long.MAX_VALUE) - assertEquals(numberElement.sign, CborInt.Sign.POSITIVE) + assertEquals(numberElement.isPositive, true) assertEquals(numberElement.value, Long.MAX_VALUE.toULong()) val numberBytes = cbor.encodeToByteArray(numberElement) val decodedNumber = cbor.decodeFromByteArray(numberBytes) @@ -66,8 +66,8 @@ class CborElementTest { @Test fun testCborNumberMin() { - val numberElement = CborInt(ULong.MAX_VALUE, sign = CborInt.Sign.NEGATIVE) - assertEquals(numberElement.sign, CborInt.Sign.NEGATIVE) + val numberElement = CborInt(ULong.MAX_VALUE, isPositive = false) + assertEquals(numberElement.isPositive, false) assertEquals(numberElement.value, ULong.MAX_VALUE) val numberBytes = cbor.encodeToByteArray(numberElement) val decodedNumber = cbor.decodeFromByteArray(numberBytes) @@ -77,14 +77,14 @@ class CborElementTest { val lossOfPrecision = cbor.decodeFromCborElement(numberElement) assertEquals(1L, lossOfPrecision) - assertEquals(1L, numberElement.toLong()) + assertEquals(-1L, numberElement.toLong()) } @Test fun testCborNumberMinHalv() { - val numberElement = CborInt(Long.MAX_VALUE.toULong(), sign = CborInt.Sign.NEGATIVE) - assertEquals(numberElement.sign, CborInt.Sign.NEGATIVE) + val numberElement = CborInt(Long.MAX_VALUE.toULong(), isPositive = false) + assertEquals(numberElement.isPositive, false) assertEquals(numberElement.value, Long.MAX_VALUE.toULong()) val numberBytes = cbor.encodeToByteArray(numberElement) val decodedNumber = cbor.decodeFromByteArray(numberBytes) @@ -94,7 +94,7 @@ class CborElementTest { val long = cbor.decodeFromCborElement(numberElement) assertEquals(Long.MIN_VALUE+1, long) - assertEquals(Long.MIN_VALUE+1, numberElement.toLong()) + assertEquals(-(Long.MIN_VALUE+1), numberElement.toLong()) } @@ -105,12 +105,6 @@ class CborElementTest { assertEquals(Long.MIN_VALUE, CborInt(Long.MIN_VALUE).toLong()) } - @Test - fun testCborNumberIllegal() { - assertFails { CborInt(ULong.MAX_VALUE, sign = CborInt.Sign.ZERO) } - assertFails { CborInt(1uL, sign = CborInt.Sign.ZERO) } - } - @Test fun testCborString() { val stringElement = CborString("Hello, CBOR!") From 7f2da40285d1d4e3b40cef49aeacafd25792233d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 16:09:50 +0100 Subject: [PATCH 31/68] Fix structured CBOR element encoding --- .../kotlinx/serialization/cbor/CborElement.kt | 27 ++++-- .../cbor/internal/CborElementSerializers.kt | 82 +++++++++++-------- .../cbor/internal/CborTreeReader.kt | 7 +- .../serialization/cbor/internal/Encoder.kt | 27 +++++- .../serialization/cbor/internal/Encoding.kt | 1 + .../serialization/cbor/CborElementTest.kt | 44 +++++++++- 6 files changed, 138 insertions(+), 50 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt index 2d9b3ca226..a3d03db114 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -112,10 +112,7 @@ public class CborInt( /** * **WARNING! Possible truncation/overflow!** E.g., `-2^64` -> `1` */ - public fun toLong(): Long = when (isPositive) { - isPositive -> value.toLong() - else -> -value.toLong() - } + public fun toLong(): Long = if (isPositive) value.toLong() else -value.toLong() public companion object { @@ -125,7 +122,7 @@ public class CborInt( * * unsigned CBOR integer (major type 0 encompassing `0..2^64-1`) * * depending on whether a positive or a negative number was passed. - * If you want to create a negative number exceeding [Long.MIN_VALUE], manually specify sign: `CborInt(ULong.MAX_VALUE, CborInt.Sign.NEGATIVE)` + * If you want to create a negative number exceeding [Long.MIN_VALUE], manually specify sign: `CborInt(ULong.MAX_VALUE, isPositive = false)` */ public operator fun invoke( value: Long, @@ -237,6 +234,13 @@ public class CborByteString( @ExperimentalSerializationApi public class CborNull(vararg tags: ULong) : CborPrimitive(Unit, tags) +/** + * Class representing CBOR `undefined` value + */ +@Serializable(with = CborUndefinedSerializer::class) +@ExperimentalSerializationApi +public class CborUndefined(vararg tags: ULong) : CborPrimitive(Unit, tags) + /** * Class representing CBOR map, consisting of key-value pairs, where both key and value are arbitrary [CborElement] * @@ -262,9 +266,14 @@ public class CborMap( ")" } - public operator fun get(key: String?): CborElement? = content[key?.let { CborString(it) }?:CborNull() ] - public operator fun get(key: Long?): CborElement? = content[key?.let { CborInt(it) }?:CborNull() ] - public operator fun get(key: Int?): CborElement? = content[key?.let { CborInt(it.toULong()) }?:CborNull() ] + public operator fun get(key: String): CborElement? = content[CborString(key)] + public fun getValue(key: String): CborElement = content.getValue(CborString(key)) + + public operator fun get(key: Long): CborElement? = content[CborInt(key)] + public fun getValue(key: Long): CborElement = content.getValue(CborInt(key)) + + public operator fun get(key: Int): CborElement? = content[CborInt(key.toLong())] + public fun getValue(key: Int): CborElement = content.getValue(CborInt(key.toLong())) } @@ -293,4 +302,4 @@ public class CborList( ")" } -} \ No newline at end of file +} diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index 9a70c36396..c4e80ca165 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -23,6 +23,7 @@ internal object CborElementSerializer : KSerializer, CborSerializer // Resolve cyclic dependency in descriptors by late binding element("CborPrimitive", defer { CborPrimitiveSerializer.descriptor }) element("CborNull", defer { CborNullSerializer.descriptor }) + element("CborUndefined", defer { CborUndefinedSerializer.descriptor }) element("CborString", defer { CborStringSerializer.descriptor }) element("CborBoolean", defer { CborBooleanSerializer.descriptor }) element("CborByteString", defer { CborByteStringSerializer.descriptor }) @@ -60,6 +61,7 @@ internal object CborPrimitiveSerializer : KSerializer>, CborSer override fun serialize(encoder: Encoder, value: CborPrimitive<*>) { when (value) { is CborNull -> encoder.encodeSerializableValue(CborNullSerializer, value) + is CborUndefined -> encoder.encodeSerializableValue(CborUndefinedSerializer, value) is CborString -> encoder.encodeSerializableValue(CborStringSerializer, value) is CborBoolean -> encoder.encodeSerializableValue(CborBooleanSerializer, value) is CborByteString -> encoder.encodeSerializableValue(CborByteStringSerializer, value) @@ -91,13 +93,29 @@ internal object CborNullSerializer : KSerializer, CborSerializer { } override fun deserialize(decoder: Decoder): CborNull { - decoder.asCborDecoder() - if (decoder.decodeNotNullMark()) { - throw CborDecodingException("Expected 'null' literal") - } + val element = decoder.asCborDecoder().decodeCborElement() + if (element !is CborNull) throw CborDecodingException("Unexpected CBOR element, expected CborNull, had ${element::class}") + return element + } +} + +internal object CborUndefinedSerializer : KSerializer, CborSerializer { + override val descriptor: SerialDescriptor = + buildSerialDescriptor("kotlinx.serialization.cbor.CborUndefined", SerialKind.ENUM) - decoder.decodeNull() - return CborNull() + override fun serialize(encoder: Encoder, value: CborUndefined) { + val cborEncoder = encoder.asCborEncoder() + cborEncoder.encodeTags(value.tags) + (cborEncoder as? CborWriter ?: throw IllegalStateException( + "This serializer can be used only with Cbor format. " + + "Expected Encoder to be internal CborWriter, got ${cborEncoder::class}" + )).encodeUndefined() + } + + override fun deserialize(decoder: Decoder): CborUndefined { + val element = decoder.asCborDecoder().decodeCborElement() + if (element !is CborUndefined) throw CborDecodingException("Unexpected CBOR element, expected CborUndefined, had ${element::class}") + return element } } @@ -135,8 +153,9 @@ internal object CborFloatSerializer : KSerializer, CborSerializer { } override fun deserialize(decoder: Decoder): CborFloat { - decoder.asCborDecoder() - return CborFloat(decoder.decodeDouble()) + val element = decoder.asCborDecoder().decodeCborElement() + if (element !is CborFloat) throw CborDecodingException("Unexpected CBOR element, expected CborFloat, had ${element::class}") + return element } } @@ -190,13 +209,16 @@ internal object CborBooleanSerializer : KSerializer, CborSerializer */ internal object CborByteStringSerializer : KSerializer, CborSerializer { override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborByteString", PrimitiveKind.STRING) + SerialDescriptor("kotlinx.serialization.cbor.CborByteString", ByteArraySerializer().descriptor) override fun serialize(encoder: Encoder, value: CborByteString) { val cborEncoder = encoder.asCborEncoder() cborEncoder.encodeTags(value.tags) //this we really don't want to expose so we cast here - (cborEncoder as CborWriter).encodeByteString(value.value) + (cborEncoder as? CborWriter ?: throw IllegalStateException( + "This serializer can be used only with Cbor format. " + + "Expected Encoder to be internal CborWriter, got ${cborEncoder::class}" + )).encodeByteString(value.value) } override fun deserialize(decoder: Decoder): CborByteString { @@ -212,13 +234,11 @@ internal object CborByteStringSerializer : KSerializer, CborSeri * It can only be used by with [Cbor] format and its input ([CborDecoder] and [CborEncoder]). */ internal object CborMapSerializer : KSerializer, CborSerializer { - private object CborMapDescriptor : - SerialDescriptor by MapSerializer(CborElementSerializer, CborElementSerializer).descriptor { - @ExperimentalSerializationApi - override val serialName: String = "kotlinx.serialization.cbor.CborMap" - } - - override val descriptor: SerialDescriptor = CborMapDescriptor + override val descriptor: SerialDescriptor = + SerialDescriptor( + serialName = "kotlinx.serialization.cbor.CborMap", + original = MapSerializer(CborElementSerializer, CborElementSerializer).descriptor + ) override fun serialize(encoder: Encoder, value: CborMap) { val cborEncoder = encoder.asCborEncoder() @@ -227,8 +247,9 @@ internal object CborMapSerializer : KSerializer, CborSerializer { } override fun deserialize(decoder: Decoder): CborMap { - decoder.asCborDecoder() - return CborMap(MapSerializer(CborElementSerializer, CborElementSerializer).deserialize(decoder)) + val element = decoder.asCborDecoder().decodeCborElement() + if (element !is CborMap) throw CborDecodingException("Unexpected CBOR element, expected CborMap, had ${element::class}") + return element } } @@ -237,12 +258,11 @@ internal object CborMapSerializer : KSerializer, CborSerializer { * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). */ internal object CborListSerializer : KSerializer, CborSerializer { - private object CborListDescriptor : SerialDescriptor by ListSerializer(CborElementSerializer).descriptor { - @ExperimentalSerializationApi - override val serialName: String = "kotlinx.serialization.cbor.CborList" - } - - override val descriptor: SerialDescriptor = CborListDescriptor + override val descriptor: SerialDescriptor = + SerialDescriptor( + serialName = "kotlinx.serialization.cbor.CborList", + original = ListSerializer(CborElementSerializer).descriptor + ) override fun serialize(encoder: Encoder, value: CborList) { val cborEncoder = encoder.asCborEncoder() @@ -251,8 +271,9 @@ internal object CborListSerializer : KSerializer, CborSerializer { } override fun deserialize(decoder: Decoder): CborList { - decoder.asCborDecoder() - return CborList(ListSerializer(CborElementSerializer).deserialize(decoder)) + val element = decoder.asCborDecoder().decodeCborElement() + if (element !is CborList) throw CborDecodingException("Unexpected CBOR element, expected CborList, had ${element::class}") + return element } } @@ -291,10 +312,3 @@ private fun defer(deferred: () -> SerialDescriptor): SerialDescriptor = object : override fun getElementDescriptor(index: Int): SerialDescriptor = original.getElementDescriptor(index) override fun isElementOptional(index: Int): Boolean = original.isElementOptional(index) } - -private fun CborWriter.encodeTags(value: CborElement) { // Encode tags if present - if (value.tags.isNotEmpty()) { - encodeTags(value.tags) - } - -} \ No newline at end of file diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt index c25a2c4f66..5e64d3c74a 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt @@ -64,10 +64,15 @@ internal class CborTreeReader( CborBoolean(true, tags = tags) } - 0xF6, 0xF7 -> { + 0xF6 -> { parser.nextNull() CborNull(tags = tags) } + + 0xF7 -> { + parser.readByte() // Advance parser position + CborUndefined(tags = tags) + } // Half/Float32/Float64 NEXT_HALF, NEXT_FLOAT, NEXT_DOUBLE -> CborFloat(parser.nextDouble(), tags = tags) else -> throw CborDecodingException( 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 c49101c515..c7b75fe2d2 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -32,6 +32,10 @@ internal sealed class CborWriter( getDestination().encodeByteString(byteArray) } + internal open fun encodeUndefined() { + getDestination().encodeUndefined() + } + protected var isClass = false protected var encodeByteArrayAsByteString = false @@ -217,9 +221,18 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { fun finalize() = when (this) { is List -> CborList(content = elements, tags = tags) is Map -> CborMap( - content = if (elements.isNotEmpty()) IntRange(0, elements.size / 2 - 1).associate { - elements[it * 2] to elements[it * 2 + 1] - } else mapOf(), + content = if (elements.isNotEmpty()) { + require(elements.size % 2 == 0) { + "Implementation error: map element list must contain alternating keys and values, but had odd size ${elements.size}." + } + buildMap(elements.size / 2) { + for (index in 0 until elements.size step 2) { + put(elements[index], elements[index + 1]) + } + } + } else { + mapOf() + }, tags = tags ) @@ -234,7 +247,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { private val stack = ArrayDeque() - private var currentElement: CborContainer? = null + private var currentElement: CborContainer? = CborContainer.Primitive(tags = ulongArrayOf()) // value tags are collects inside beginStructure, so we need to cache them here and write them in beginStructure or encodeXXX // and then null them out, so there are no leftovers @@ -366,6 +379,10 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { currentElement += CborByteString(byteArray, tags = nextValueTags) } + override fun encodeUndefined() { + currentElement += CborUndefined(tags = nextValueTags) + } + override fun encodeNull() { /*NOT CBOR-COMPLIANT, KxS-proprietary behaviour*/ currentElement += if (isClass) CborMap( @@ -446,6 +463,8 @@ internal fun ByteArrayOutput.end() = write(BREAK) internal fun ByteArrayOutput.encodeNull() = write(NULL) +internal fun ByteArrayOutput.encodeUndefined() = write(UNDEFINED) + internal fun ByteArrayOutput.encodeEmptyMap() = write(EMPTY_MAP) internal fun ByteArrayOutput.writeByte(byteValue: Int) = write(byteValue) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoding.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoding.kt index 72b67465eb..2e32d2aff6 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoding.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoding.kt @@ -12,6 +12,7 @@ import kotlinx.serialization.descriptors.* internal const val FALSE = 0xf4 internal const val TRUE = 0xf5 internal const val NULL = 0xf6 +internal const val UNDEFINED = 0xf7 internal const val EMPTY_MAP = 0xa0 internal const val NEXT_HALF = 0xf9 diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index 45f00015c7..99801368dd 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -12,6 +12,21 @@ class CborElementTest { private val cbor = Cbor {} + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testEncodeToCborElementRootPrimitiveInt() { + assertEquals(CborInt(42), cbor.encodeToCborElement(42)) + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testEncodeToCborElementRootPrimitiveByteArrayAlwaysUseByteString() { + val configured = Cbor { alwaysUseByteString = true } + val element = configured.encodeToCborElement(byteArrayOf(1, 2, 3)) + assertTrue(element is CborByteString) + assertTrue(element.value.contentEquals(byteArrayOf(1, 2, 3))) + } + @Test fun testCborNull() { val nullElement = CborNull() @@ -77,7 +92,7 @@ class CborElementTest { val lossOfPrecision = cbor.decodeFromCborElement(numberElement) assertEquals(1L, lossOfPrecision) - assertEquals(-1L, numberElement.toLong()) + assertEquals(1L, numberElement.toLong()) } @@ -94,7 +109,7 @@ class CborElementTest { val long = cbor.decodeFromCborElement(numberElement) assertEquals(Long.MIN_VALUE+1, long) - assertEquals(-(Long.MIN_VALUE+1), numberElement.toLong()) + assertEquals(Long.MIN_VALUE+1, numberElement.toLong()) } @@ -690,6 +705,31 @@ class CborElementTest { } } + @OptIn(ExperimentalUnsignedTypes::class) + @Test + fun testCborUndefinedRoundTrip() { + val element = CborUndefined(1uL) + val bytes = cbor.encodeToByteArray(element) + assertEquals("c1f7", bytes.toHexString()) + assertEquals(element, cbor.decodeFromByteArray(bytes)) + } + + @OptIn(ExperimentalUnsignedTypes::class) + @Test + fun testTagsPreservedWhenDecodingTypedElements() { + val taggedMap = CborMap(mapOf(CborString("a") to CborInt(1)), 1uL) + assertEquals(taggedMap, cbor.decodeFromByteArray(cbor.encodeToByteArray(taggedMap))) + + val taggedList = CborList(listOf(CborInt(1)), 2uL) + assertEquals(taggedList, cbor.decodeFromByteArray(cbor.encodeToByteArray(taggedList))) + + val taggedFloat = CborFloat(1.5, 3uL) + assertEquals(taggedFloat, cbor.decodeFromByteArray(cbor.encodeToByteArray(taggedFloat))) + + val taggedNull = CborNull(4uL) + assertEquals(taggedNull, cbor.decodeFromByteArray(cbor.encodeToByteArray(taggedNull))) + } + } @Serializable From ed9ae5e5371fd89ca2ebcfc29ab15b5e947fce2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 16:18:24 +0100 Subject: [PATCH 32/68] Expose byte string and undefined encoding --- .../src/kotlinx/serialization/cbor/Cbor.kt | 2 +- .../src/kotlinx/serialization/cbor/CborEncoder.kt | 12 ++++++++++++ .../cbor/internal/CborElementSerializers.kt | 11 ++--------- .../kotlinx/serialization/cbor/internal/Encoder.kt | 4 ++-- .../kotlinx/serialization/cbor/CborElementTest.kt | 14 ++++++++++++++ 5 files changed, 31 insertions(+), 12 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt index 2062dcc41c..01b0d22381 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt @@ -131,7 +131,7 @@ public inline fun Cbor.encodeToCborElement(value: T): CborElement = * Deserializes the given [element] element into a value of type [T] using a deserializer retrieved * from reified type parameter. * - * @throws [SerializationException] if the given JSON element is not a valid CBOR input for the type [T] + * @throws [SerializationException] if the given CBOR element is not a valid CBOR input for the type [T] * @throws [IllegalArgumentException] if the decoded input cannot be represented as a valid instance of type [T] */ @ExperimentalSerializationApi diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt index 308ae48682..caf777fa64 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt @@ -69,6 +69,18 @@ public interface CborEncoder : Encoder { */ public fun encodeTags(@OptIn(kotlin.ExperimentalUnsignedTypes::class) tags: ULongArray): Unit + /** + * Encode a CBOR byte string (major type 2). + * + * This exists for low-level CBOR serializers and for encoding [CborByteString] values. + */ + public fun encodeByteString(byteArray: ByteArray) + + /** + * Encode CBOR `undefined` (simple value 23 / `0xF7`). + */ + public fun encodeUndefined() + /** * Encode a negative value as [CborInt]. This function exists to encode negative values exceeding [Long.MIN_VALUE] */ diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index c4e80ca165..13ec0760a4 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -106,10 +106,7 @@ internal object CborUndefinedSerializer : KSerializer, CborSerial override fun serialize(encoder: Encoder, value: CborUndefined) { val cborEncoder = encoder.asCborEncoder() cborEncoder.encodeTags(value.tags) - (cborEncoder as? CborWriter ?: throw IllegalStateException( - "This serializer can be used only with Cbor format. " + - "Expected Encoder to be internal CborWriter, got ${cborEncoder::class}" - )).encodeUndefined() + cborEncoder.encodeUndefined() } override fun deserialize(decoder: Decoder): CborUndefined { @@ -214,11 +211,7 @@ internal object CborByteStringSerializer : KSerializer, CborSeri override fun serialize(encoder: Encoder, value: CborByteString) { val cborEncoder = encoder.asCborEncoder() cborEncoder.encodeTags(value.tags) - //this we really don't want to expose so we cast here - (cborEncoder as? CborWriter ?: throw IllegalStateException( - "This serializer can be used only with Cbor format. " + - "Expected Encoder to be internal CborWriter, got ${cborEncoder::class}" - )).encodeByteString(value.value) + cborEncoder.encodeByteString(value.value) } override fun deserialize(decoder: Decoder): CborByteString { 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 c7b75fe2d2..705c021696 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -28,11 +28,11 @@ internal sealed class CborWriter( override val cbor: Cbor, ) : AbstractEncoder(), CborEncoder { - internal open fun encodeByteString(byteArray: ByteArray) { + override fun encodeByteString(byteArray: ByteArray) { getDestination().encodeByteString(byteArray) } - internal open fun encodeUndefined() { + override fun encodeUndefined() { getDestination().encodeUndefined() } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index 99801368dd..ccc48d4c2a 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -27,6 +27,20 @@ class CborElementTest { assertTrue(element.value.contentEquals(byteArrayOf(1, 2, 3))) } + @Serializable + private data class Wrapped(val x: Int) + + @Serializable + private data class Wrapper(val datum: Wrapped?) + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testEncodeDecodeNullableClassViaCborElement() { + val wrapper = Wrapper(null) + val element = cbor.encodeToCborElement(wrapper) + assertEquals(wrapper, cbor.decodeFromCborElement(element)) + } + @Test fun testCborNull() { val nullElement = CborNull() From af1183c0ef7545ae929e39b83b066b95811f2687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Mon, 15 Dec 2025 11:08:47 +0100 Subject: [PATCH 33/68] CBOR: Optionally encode nullable complex props as empty map (fixes #2848) --- docs/formats.md | 6 + docs/serialization-guide.md | 1 + .../cbor/api/kotlinx-serialization-cbor.api | 8 +- .../api/kotlinx-serialization-cbor.klib.api | 4 + .../serialization/cbor/CborNullAsEmptyMap.kt | 42 +++++ .../serialization/cbor/internal/Encoder.kt | 14 +- .../cbor/CborNullAsEmptyMapTest.kt | 178 ++++++++++++++++++ 7 files changed, 243 insertions(+), 10 deletions(-) create mode 100644 formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborNullAsEmptyMap.kt create mode 100644 formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborNullAsEmptyMapTest.kt diff --git a/docs/formats.md b/docs/formats.md index d2b6a9ed27..934201cf35 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -16,6 +16,7 @@ stable, these are currently experimental features of Kotlin Serialization. * [Definite vs. Indefinite Length Encoding](#definite-vs-indefinite-length-encoding) * [Tags and Labels](#tags-and-labels) * [Arrays](#arrays) + * [Nullability of Properties](#nullability-of-properties) * [Custom CBOR-specific Serializers](#custom-cbor-specific-serializers) * [CBOR Elements](#cbor-elements) * [Encoding from/to `CborElement`](#encoding-fromto-cborelement) @@ -313,6 +314,11 @@ When annotated with `@CborArray`, serialization of the same object will produce ``` This may be used to encode COSE structures, see [RFC 9052 2. Basic COSE Structure](https://www.rfc-editor.org/rfc/rfc9052#section-2). +### Nullability of Properties +Some standards, like COSE, tend to encode the absence of a complex property as an empty map (because the complex property itself +consists only of nullable properties). This cannot be modelled elegantly, such that the null-safety of Kotlin can be leveraged. +To work around this, complex nullable properties can be annotated with [`@CborNullAsEmptyMap`](CborNullAsEmptyMap.kt), to emulate this behaviour. + ### Custom CBOR-specific Serializers Cbor encoders and decoders implement the interfaces [CborEncoder](CborEncoder.kt) and [CborDecoder](CborDecoder.kt), respectively. These interfaces contain a single property, `cbor`, exposing the current CBOR serialization configuration. diff --git a/docs/serialization-guide.md b/docs/serialization-guide.md index d03f518f01..312dee62a1 100644 --- a/docs/serialization-guide.md +++ b/docs/serialization-guide.md @@ -154,6 +154,7 @@ Once the project is set up, we can start serializing some classes. * [Definite vs. Indefinite Length Encoding](formats.md#definite-vs-indefinite-length-encoding) * [Tags and Labels](formats.md#tags-and-labels) * [Arrays](formats.md#arrays) + * [Nullability of Properties](formats.md#nullability-of-properties) * [Custom CBOR-specific Serializers](formats.md#custom-cbor-specific-serializers) * [CBOR Elements](formats.md#cbor-elements) * [Encoding from/to `CborElement`](formats.md#encoding-fromto-cborelement) diff --git a/formats/cbor/api/kotlinx-serialization-cbor.api b/formats/cbor/api/kotlinx-serialization-cbor.api index 023bfe5b59..ee934e8e90 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.api @@ -27,6 +27,13 @@ public final synthetic class kotlinx/serialization/cbor/CborArray$Impl : kotlinx public fun ()V } +public abstract interface annotation class kotlinx/serialization/cbor/CborNullAsEmptyMap : java/lang/annotation/Annotation { +} + +public final synthetic class kotlinx/serialization/cbor/CborNullAsEmptyMap$Impl : kotlinx/serialization/cbor/CborNullAsEmptyMap { + public fun ()V +} + public final class kotlinx/serialization/cbor/CborBoolean : kotlinx/serialization/cbor/CborPrimitive { public static final field Companion Lkotlinx/serialization/cbor/CborBoolean$Companion; public synthetic fun (Z[JLkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -352,4 +359,3 @@ public final synthetic class kotlinx/serialization/cbor/ValueTags$Impl : kotlinx public synthetic fun ([JLkotlin/jvm/internal/DefaultConstructorMarker;)V public final synthetic fun tags ()[J } - diff --git a/formats/cbor/api/kotlinx-serialization-cbor.klib.api b/formats/cbor/api/kotlinx-serialization-cbor.klib.api index 0d0319aaff..c1a6b3fa62 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.klib.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.klib.api @@ -21,6 +21,10 @@ open annotation class kotlinx.serialization.cbor/CborLabel : kotlin/Annotation { final fun (): kotlin/Long // kotlinx.serialization.cbor/CborLabel.label.|(){}[0] } +open annotation class kotlinx.serialization.cbor/CborNullAsEmptyMap : kotlin/Annotation { // kotlinx.serialization.cbor/CborNullAsEmptyMap|null[0] + constructor () // kotlinx.serialization.cbor/CborNullAsEmptyMap.|(){}[0] +} + open annotation class kotlinx.serialization.cbor/KeyTags : kotlin/Annotation { // kotlinx.serialization.cbor/KeyTags|null[0] constructor (kotlin/ULongArray...) // kotlinx.serialization.cbor/KeyTags.|(kotlin.ULongArray...){}[0] diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborNullAsEmptyMap.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborNullAsEmptyMap.kt new file mode 100644 index 0000000000..03b46f089a --- /dev/null +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborNullAsEmptyMap.kt @@ -0,0 +1,42 @@ +package kotlinx.serialization.cbor + +import kotlinx.serialization.* + +/** + * Marks a complex property to be encoded as an empty map when null, instead of CBOR `null`. + * + * This is useful for COSE encoding, because COSE known protected and unprotected headers, for example, + * and the compiler handles null checks, while checks for empty maps would lead to duplicated spaghetti code. + * + * Example usage: + * + * ``` + * + * @Serializable + * data class ClassWNullableAsMap( + * @SerialName("nullable") + * @CborNullAsEmptyMap + * val nullable: NullableClass? + * ) + * + * @Serializable + * data class NullableClass( + * val property: String + * ) + * + * Cbor.encodeToByteArray(ClassWNullableAsMap(nullable = null)) + * ``` + * + * will produce bytes `0xbf686e756c6c61626c65a0ff`, or in diagnostic notation: + * + * ``` + *a1 # map(1) + * 68 # text(8) + * 6e756c6c61626c65 # "nullable" + * a0 # map(0) + * ``` + */ +@SerialInfo +@Target(AnnotationTarget.PROPERTY) +@ExperimentalSerializationApi +public annotation class CborNullAsEmptyMap 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 705c021696..bfc437bfef 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -36,7 +36,7 @@ internal sealed class CborWriter( getDestination().encodeUndefined() } - protected var isClass = false + protected var encodeNullAsEmptyMap = false protected var encodeByteArrayAsByteString = false @@ -112,7 +112,7 @@ internal sealed class CborWriter( override fun encodeNull() { - if (isClass) getDestination().encodeEmptyMap() + if (encodeNullAsEmptyMap) getDestination().encodeEmptyMap() else getDestination().encodeNull() } @@ -126,7 +126,7 @@ internal sealed class CborWriter( override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean { val destination = getDestination() - isClass = descriptor.getElementDescriptor(index).kind == StructureKind.CLASS + encodeNullAsEmptyMap = descriptor.getElementAnnotations(index).find { it is CborNullAsEmptyMap } != null encodeByteArrayAsByteString = descriptor.isByteString(index) val name = descriptor.getElementName(index) @@ -294,10 +294,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean { - // this mirrors the special encoding of nullable classes that are null into am empty map. - // THIS IS NOT CBOR-COMPLiANT - // but keeps backwards compatibility with the way kotlinx.serialization CBOR format has always worked. - isClass = descriptor.getElementDescriptor(index).kind == StructureKind.CLASS + encodeNullAsEmptyMap = descriptor.getElementAnnotations(index).find { it is CborNullAsEmptyMap } != null encodeByteArrayAsByteString = descriptor.isByteString(index) //TODO check if cborelement and be done @@ -384,8 +381,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { } override fun encodeNull() { - /*NOT CBOR-COMPLIANT, KxS-proprietary behaviour*/ - currentElement += if (isClass) CborMap( + currentElement += if (encodeNullAsEmptyMap) CborMap( mapOf(), tags = nextValueTags ) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborNullAsEmptyMapTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborNullAsEmptyMapTest.kt new file mode 100644 index 0000000000..445f97e962 --- /dev/null +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborNullAsEmptyMapTest.kt @@ -0,0 +1,178 @@ +package kotlinx.serialization.cbor + +import kotlinx.serialization.* +import kotlin.test.* + + +class CborNullAsEmptyMapTest { + + + @Test + fun nullableAsMap() { + /** + * a1 # map(1) + * 68 # text(8) + * 6e756c6c61626c65 # "nullable" + * a0 # map(0) + */ + val referenceHexString = "a1686e756c6c61626c65a0" + val reference = ClassWNullableAsMap(nullable = null) + + val cbor = Cbor.CoseCompliant + + assertEquals(referenceHexString, cbor.encodeToHexString(ClassWNullableAsMap.serializer(), reference)) + assertEquals(reference, cbor.decodeFromHexString(ClassWNullableAsMap.serializer(), referenceHexString)) + } + + @Test + fun nullableAsNull() { + /** + * a1 # map(1) + * 68 # text(8) + * 6e756c6c61626c65 # "nullable" + * f6 # null, simple(22) + */ + val referenceHexString = "a1686e756c6c61626c65f6" + val reference = ClassWNullableAsNull(nullable = null) + + + val cbor = Cbor.CoseCompliant + + assertEquals(referenceHexString, cbor.encodeToHexString(ClassWNullableAsNull.serializer(), reference)) + assertEquals(reference, cbor.decodeFromHexString(ClassWNullableAsNull.serializer(), referenceHexString)) + } + + @Test + fun nullableAsMapWithDefaultNull() { + /** + * a1 # map(1) + * 68 # text(8) + * 6e756c6c61626c65 # "nullable" + * a0 # map(0) + */ + val referenceHexString = "a1686e756c6c61626c65a0" + val reference = ClassWNullableAsMapWithDefaultValueNull() + + val cbor = Cbor { + useDefiniteLengthEncoding = true + encodeDefaults = true + } + + assertEquals( + referenceHexString, + cbor.encodeToHexString(ClassWNullableAsMapWithDefaultValueNull.serializer(), reference) + ) + assertEquals( + reference, + cbor.decodeFromHexString(ClassWNullableAsMapWithDefaultValueNull.serializer(), referenceHexString) + ) + } + + @Test + fun nullableAsNullWithDefaultNull() { + /** + * a1 # map(1) + * 68 # text(8) + * 6e756c6c61626c65 # "nullable" + * f6 # null, simple(22) + */ + val referenceHexString = "a1686e756c6c61626c65f6" + val reference = ClassWNullableAsNullWithDefaultValueNull() + + + val cbor = Cbor { + useDefiniteLengthEncoding = true + encodeDefaults = true + } + + assertEquals( + referenceHexString, + cbor.encodeToHexString(ClassWNullableAsNullWithDefaultValueNull.serializer(), reference) + ) + assertEquals( + reference, + cbor.decodeFromHexString(ClassWNullableAsNullWithDefaultValueNull.serializer(), referenceHexString) + ) + } + @Test + fun nullableAsMapWithDefaultNullNoEncodeDefaults() { + /** + * a0 # map(0) + */ + val referenceHexString = "a0" + val reference = ClassWNullableAsMapWithDefaultValueNull() + + val cbor = Cbor { + useDefiniteLengthEncoding = true + encodeDefaults = false + } + + assertEquals( + referenceHexString, + cbor.encodeToHexString(ClassWNullableAsMapWithDefaultValueNull.serializer(), reference) + ) + assertEquals( + reference, + cbor.decodeFromHexString(ClassWNullableAsMapWithDefaultValueNull.serializer(), referenceHexString) + ) + } + + @Test + fun nullableAsNullWithDefaultNullNoEncodeDefaults() { + /** + * a0 # map(0) + */ + val referenceHexString = "a0" + val reference = ClassWNullableAsNullWithDefaultValueNull() + + + val cbor = Cbor { + useDefiniteLengthEncoding = true + encodeDefaults = false + } + + assertEquals( + referenceHexString, + cbor.encodeToHexString(ClassWNullableAsNullWithDefaultValueNull.serializer(), reference) + ) + assertEquals( + reference, + cbor.decodeFromHexString(ClassWNullableAsNullWithDefaultValueNull.serializer(), referenceHexString) + ) + } +} + +@Serializable +data class ClassWNullableAsMap( + @SerialName("nullable") + @CborNullAsEmptyMap + val nullable: NullableClass? +) + +@Serializable +data class ClassWNullableAsMapWithDefaultValueNull( + @SerialName("nullable") + @CborNullAsEmptyMap + val nullable: NullableClass? = null +) + + +@Serializable +data class ClassWNullableAsNull( + @SerialName("nullable") + val nullable: NullableClass? +) + +@Serializable +data class ClassWNullableAsNullWithDefaultValueNull( + @SerialName("nullable") + val nullable: NullableClass? = null +) + +@Serializable +data class NullableClass( + val property: String +) + + + From 60a45f1ef0bd9c848035a370420e8fd7ad23c85b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 16:52:12 +0100 Subject: [PATCH 34/68] Adjust null object encoding expectations --- .../src/kotlinx/serialization/cbor/CborArrayTest.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborArrayTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborArrayTest.kt index 025fd9be2a..f16005de33 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborArrayTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborArrayTest.kt @@ -53,9 +53,9 @@ class CborArrayTest { * 63 # text(3) * 626172 # "bar" * F6 # primitive(22) - * A0 # map(0) + * F6 # primitive(22) */ - val referenceHexString = "842663626172f6a0" + val referenceHexString = "842663626172f6f6" val reference = ClassAs4ArrayNullable(alg = -7, kid = "bar", iv = null, array = null) val cbor = Cbor.CoseCompliant @@ -192,4 +192,3 @@ class CborArrayTest { val array: ClassAs2Array, ) } - From 6c54c0c3060672a55f8e184e543a04af5b2320ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 17:03:43 +0100 Subject: [PATCH 35/68] Reuse shared empty tags array --- .../kotlinx/serialization/cbor/CborElement.kt | 7 +++++-- .../serialization/cbor/internal/Encoder.kt | 17 ++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt index a3d03db114..3c3a9469c8 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -10,6 +10,9 @@ package kotlinx.serialization.cbor import kotlinx.serialization.* import kotlinx.serialization.cbor.internal.* +@OptIn(ExperimentalUnsignedTypes::class) +internal val EMPTY_TAGS: ULongArray = ULongArray(0) + /** * Class representing single CBOR element. * Can be [CborPrimitive], [CborMap] or [CborList]. @@ -29,7 +32,7 @@ public sealed class CborElement( * See [RFC 8949 3.4. Tagging of Items](https://datatracker.ietf.org/doc/html/rfc8949#name-tagging-of-items). */ @OptIn(ExperimentalUnsignedTypes::class) - tags: ULongArray = ulongArrayOf() + tags: ULongArray = EMPTY_TAGS ) { /** @@ -64,7 +67,7 @@ public sealed class CborElement( @ExperimentalSerializationApi public sealed class CborPrimitive( public val value: T, - tags: ULongArray = ulongArrayOf() + tags: ULongArray = EMPTY_TAGS ) : CborElement(tags) { override fun equals(other: Any?): Boolean { if (this === other) return true 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 bfc437bfef..52c7fc7506 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -245,16 +245,15 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { this!!.add(element) } - private val stack = ArrayDeque() - private var currentElement: CborContainer? = CborContainer.Primitive(tags = ulongArrayOf()) + private var currentElement: CborContainer? = CborContainer.Primitive(tags = EMPTY_TAGS) // value tags are collects inside beginStructure, so we need to cache them here and write them in beginStructure or encodeXXX // and then null them out, so there are no leftovers - private var nextValueTags: ULongArray = ulongArrayOf() + private var nextValueTags: ULongArray = EMPTY_TAGS get() { val ret = field - field = ulongArrayOf() + field = EMPTY_TAGS return ret } @@ -262,8 +261,8 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { val tags = nextValueTags + - if (cbor.configuration.encodeObjectTags) descriptor.getObjectTags() ?: ulongArrayOf() - else ulongArrayOf() + if (cbor.configuration.encodeObjectTags) descriptor.getObjectTags() ?: EMPTY_TAGS + else EMPTY_TAGS val element = if (descriptor.hasArrayTag()) { CborContainer.List(tags) } else { @@ -306,9 +305,9 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { //indices are put into the name field. we don't want to write those, as it would result in double writes val cborLabel = descriptor.getCborLabel(index) if (cbor.configuration.preferCborLabelsOverNames && cborLabel != null) { - currentElement += CborInt(value = cborLabel, tags = keyTags ?: ulongArrayOf()) + currentElement += CborInt(value = cborLabel, tags = keyTags ?: EMPTY_TAGS) } else { - currentElement += CborString(name, tags = keyTags ?: ulongArrayOf()) + currentElement += CborString(name, tags = keyTags ?: EMPTY_TAGS) } } } @@ -316,7 +315,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { if (cbor.configuration.encodeValueTags) { descriptor.getValueTags(index).let { valueTags -> //collect them for late encoding in beginStructure or encodeXXX - nextValueTags = valueTags ?: ulongArrayOf() + nextValueTags = valueTags ?: EMPTY_TAGS } } return true From 6ab61e8d2a28b4b5226d16fe3b47903e6bb10136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 17:08:24 +0100 Subject: [PATCH 36/68] Update CBOR API dumps --- .../cbor/api/kotlinx-serialization-cbor.api | 45 +++++++++++-------- .../api/kotlinx-serialization-cbor.klib.api | 34 +++++++------- 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/formats/cbor/api/kotlinx-serialization-cbor.api b/formats/cbor/api/kotlinx-serialization-cbor.api index ee934e8e90..85d858cb1a 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.api @@ -27,13 +27,6 @@ public final synthetic class kotlinx/serialization/cbor/CborArray$Impl : kotlinx public fun ()V } -public abstract interface annotation class kotlinx/serialization/cbor/CborNullAsEmptyMap : java/lang/annotation/Annotation { -} - -public final synthetic class kotlinx/serialization/cbor/CborNullAsEmptyMap$Impl : kotlinx/serialization/cbor/CborNullAsEmptyMap { - public fun ()V -} - public final class kotlinx/serialization/cbor/CborBoolean : kotlinx/serialization/cbor/CborPrimitive { public static final field Companion Lkotlinx/serialization/cbor/CborBoolean$Companion; public synthetic fun (Z[JLkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -121,10 +114,12 @@ public final class kotlinx/serialization/cbor/CborElement$Companion { } public abstract interface class kotlinx/serialization/cbor/CborEncoder : kotlinx/serialization/encoding/Encoder { + public abstract fun encodeByteString ([B)V public fun encodeCborElement (Lkotlinx/serialization/cbor/CborElement;)V public abstract fun encodeNegative-VKZWuLQ (J)V public abstract fun encodePositive-VKZWuLQ (J)V public abstract fun encodeTags-QwZRm1k ([J)V + public abstract fun encodeUndefined ()V public abstract fun getCbor ()Lkotlinx/serialization/cbor/Cbor; } @@ -147,10 +142,10 @@ public final class kotlinx/serialization/cbor/CborFloat$Companion { public final class kotlinx/serialization/cbor/CborInt : kotlinx/serialization/cbor/CborPrimitive { public static final field Companion Lkotlinx/serialization/cbor/CborInt$Companion; - public synthetic fun (JLkotlinx/serialization/cbor/CborInt$Sign;[JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JZ[JLkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z - public final fun getSign ()Lkotlinx/serialization/cbor/CborInt$Sign; public fun hashCode ()I + public final fun isPositive ()Z public final fun toLong ()J public fun toString ()Ljava/lang/String; } @@ -161,15 +156,6 @@ public final class kotlinx/serialization/cbor/CborInt$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class kotlinx/serialization/cbor/CborInt$Sign : java/lang/Enum { - public static final field NEGATIVE Lkotlinx/serialization/cbor/CborInt$Sign; - public static final field POSITIVE Lkotlinx/serialization/cbor/CborInt$Sign; - public static final field ZERO Lkotlinx/serialization/cbor/CborInt$Sign; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public static fun valueOf (Ljava/lang/String;)Lkotlinx/serialization/cbor/CborInt$Sign; - public static fun values ()[Lkotlinx/serialization/cbor/CborInt$Sign; -} - public final class kotlinx/serialization/cbor/CborKt { public static final fun Cbor (Lkotlinx/serialization/cbor/Cbor;Lkotlin/jvm/functions/Function1;)Lkotlinx/serialization/cbor/Cbor; public static synthetic fun Cbor$default (Lkotlinx/serialization/cbor/Cbor;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/serialization/cbor/Cbor; @@ -246,12 +232,18 @@ public final class kotlinx/serialization/cbor/CborMap : kotlinx/serialization/cb public fun containsValue (Lkotlinx/serialization/cbor/CborElement;)Z public final fun entrySet ()Ljava/util/Set; public fun equals (Ljava/lang/Object;)Z + public final fun get (I)Lkotlinx/serialization/cbor/CborElement; + public final fun get (J)Lkotlinx/serialization/cbor/CborElement; public final synthetic fun get (Ljava/lang/Object;)Ljava/lang/Object; public final fun get (Ljava/lang/Object;)Lkotlinx/serialization/cbor/CborElement; + public final fun get (Ljava/lang/String;)Lkotlinx/serialization/cbor/CborElement; public fun get (Lkotlinx/serialization/cbor/CborElement;)Lkotlinx/serialization/cbor/CborElement; public fun getEntries ()Ljava/util/Set; public fun getKeys ()Ljava/util/Set; public fun getSize ()I + public final fun getValue (I)Lkotlinx/serialization/cbor/CborElement; + public final fun getValue (J)Lkotlinx/serialization/cbor/CborElement; + public final fun getValue (Ljava/lang/String;)Lkotlinx/serialization/cbor/CborElement; public fun getValues ()Ljava/util/Collection; public fun hashCode ()I public fun isEmpty ()Z @@ -289,6 +281,13 @@ public final class kotlinx/serialization/cbor/CborNull$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public abstract interface annotation class kotlinx/serialization/cbor/CborNullAsEmptyMap : java/lang/annotation/Annotation { +} + +public final synthetic class kotlinx/serialization/cbor/CborNullAsEmptyMap$Impl : kotlinx/serialization/cbor/CborNullAsEmptyMap { + public fun ()V +} + public abstract class kotlinx/serialization/cbor/CborPrimitive : kotlinx/serialization/cbor/CborElement { public static final field Companion Lkotlinx/serialization/cbor/CborPrimitive$Companion; public synthetic fun (Ljava/lang/Object;[JILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -333,6 +332,15 @@ public final class kotlinx/serialization/cbor/CborTag { public static final field URI J } +public final class kotlinx/serialization/cbor/CborUndefined : kotlinx/serialization/cbor/CborPrimitive { + public static final field Companion Lkotlinx/serialization/cbor/CborUndefined$Companion; + public synthetic fun ([JLkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class kotlinx/serialization/cbor/CborUndefined$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public abstract interface annotation class kotlinx/serialization/cbor/KeyTags : java/lang/annotation/Annotation { public abstract fun tags ()[J } @@ -359,3 +367,4 @@ public final synthetic class kotlinx/serialization/cbor/ValueTags$Impl : kotlinx public synthetic fun ([JLkotlin/jvm/internal/DefaultConstructorMarker;)V public final synthetic fun tags ()[J } + diff --git a/formats/cbor/api/kotlinx-serialization-cbor.klib.api b/formats/cbor/api/kotlinx-serialization-cbor.klib.api index c1a6b3fa62..fafb29cad2 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.klib.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.klib.api @@ -57,9 +57,11 @@ abstract interface kotlinx.serialization.cbor/CborEncoder : kotlinx.serializatio abstract val cbor // kotlinx.serialization.cbor/CborEncoder.cbor|{}cbor[0] abstract fun (): kotlinx.serialization.cbor/Cbor // kotlinx.serialization.cbor/CborEncoder.cbor.|(){}[0] + abstract fun encodeByteString(kotlin/ByteArray) // kotlinx.serialization.cbor/CborEncoder.encodeByteString|encodeByteString(kotlin.ByteArray){}[0] abstract fun encodeNegative(kotlin/ULong) // kotlinx.serialization.cbor/CborEncoder.encodeNegative|encodeNegative(kotlin.ULong){}[0] abstract fun encodePositive(kotlin/ULong) // kotlinx.serialization.cbor/CborEncoder.encodePositive|encodePositive(kotlin.ULong){}[0] abstract fun encodeTags(kotlin/ULongArray) // kotlinx.serialization.cbor/CborEncoder.encodeTags|encodeTags(kotlin.ULongArray){}[0] + abstract fun encodeUndefined() // kotlinx.serialization.cbor/CborEncoder.encodeUndefined|encodeUndefined(){}[0] open fun encodeCborElement(kotlinx.serialization.cbor/CborElement) // kotlinx.serialization.cbor/CborEncoder.encodeCborElement|encodeCborElement(kotlinx.serialization.cbor.CborElement){}[0] } @@ -158,28 +160,16 @@ final class kotlinx.serialization.cbor/CborFloat : kotlinx.serialization.cbor/Cb } final class kotlinx.serialization.cbor/CborInt : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborInt|null[0] - constructor (kotlin/ULong, kotlinx.serialization.cbor/CborInt.Sign, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborInt.|(kotlin.ULong;kotlinx.serialization.cbor.CborInt.Sign;kotlin.ULongArray...){}[0] + constructor (kotlin/ULong, kotlin/Boolean, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborInt.|(kotlin.ULong;kotlin.Boolean;kotlin.ULongArray...){}[0] - final val sign // kotlinx.serialization.cbor/CborInt.sign|{}sign[0] - final fun (): kotlinx.serialization.cbor/CborInt.Sign // kotlinx.serialization.cbor/CborInt.sign.|(){}[0] + final val isPositive // kotlinx.serialization.cbor/CborInt.isPositive|{}isPositive[0] + final fun (): kotlin/Boolean // kotlinx.serialization.cbor/CborInt.isPositive.|(){}[0] final fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborInt.equals|equals(kotlin.Any?){}[0] final fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborInt.hashCode|hashCode(){}[0] final fun toLong(): kotlin/Long // kotlinx.serialization.cbor/CborInt.toLong|toLong(){}[0] final fun toString(): kotlin/String // kotlinx.serialization.cbor/CborInt.toString|toString(){}[0] - final enum class Sign : kotlin/Enum { // kotlinx.serialization.cbor/CborInt.Sign|null[0] - enum entry NEGATIVE // kotlinx.serialization.cbor/CborInt.Sign.NEGATIVE|null[0] - enum entry POSITIVE // kotlinx.serialization.cbor/CborInt.Sign.POSITIVE|null[0] - enum entry ZERO // kotlinx.serialization.cbor/CborInt.Sign.ZERO|null[0] - - final val entries // kotlinx.serialization.cbor/CborInt.Sign.entries|#static{}entries[0] - final fun (): kotlin.enums/EnumEntries // kotlinx.serialization.cbor/CborInt.Sign.entries.|#static(){}[0] - - final fun valueOf(kotlin/String): kotlinx.serialization.cbor/CborInt.Sign // kotlinx.serialization.cbor/CborInt.Sign.valueOf|valueOf#static(kotlin.String){}[0] - final fun values(): kotlin/Array // kotlinx.serialization.cbor/CborInt.Sign.values|values#static(){}[0] - } - final object Companion { // kotlinx.serialization.cbor/CborInt.Companion|null[0] final fun invoke(kotlin/Long, kotlin/ULongArray...): kotlinx.serialization.cbor/CborInt // kotlinx.serialization.cbor/CborInt.Companion.invoke|invoke(kotlin.Long;kotlin.ULongArray...){}[0] final fun invoke(kotlin/ULong, kotlin/ULongArray...): kotlinx.serialization.cbor/CborInt // kotlinx.serialization.cbor/CborInt.Companion.invoke|invoke(kotlin.ULong;kotlin.ULongArray...){}[0] @@ -230,7 +220,13 @@ final class kotlinx.serialization.cbor/CborMap : kotlin.collections/Map { // kotlinx.serialization.cbor/CborUndefined|null[0] + constructor (kotlin/ULongArray...) // kotlinx.serialization.cbor/CborUndefined.|(kotlin.ULongArray...){}[0] + + final object Companion { // kotlinx.serialization.cbor/CborUndefined.Companion|null[0] + final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborUndefined.Companion.serializer|serializer(){}[0] + } +} + sealed class <#A: kotlin/Any> kotlinx.serialization.cbor/CborPrimitive : kotlinx.serialization.cbor/CborElement { // kotlinx.serialization.cbor/CborPrimitive|null[0] final val value // kotlinx.serialization.cbor/CborPrimitive.value|{}value[0] final fun (): #A // kotlinx.serialization.cbor/CborPrimitive.value.|(){}[0] From 00faef35ae9df5f9133ebd51a24e0953bb46ddc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 17:14:04 +0100 Subject: [PATCH 37/68] Use shared empty tags in decoder --- .../src/kotlinx/serialization/cbor/internal/Decoder.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 a5f539a8d2..d22ad070b9 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -120,7 +120,7 @@ internal open class CborReader(override val cbor: Cbor, protected val parser: Cb return if (deserializer is CborSerializer) { val tags = parser.processTags(tags) decodeCborElement().also { /*this is a NOOP for structured parser but not from bytes */it.tags = - tags ?: ulongArrayOf() + tags ?: EMPTY_TAGS } as T } else if ((decodeByteArrayAsByteString || cbor.configuration.alwaysUseByteString) && deserializer.descriptor == ByteArraySerializer().descriptor @@ -851,4 +851,4 @@ private fun SerialDescriptor.getElementIndexOrThrow(name: String): Int { " You can enable 'CborBuilder.ignoreUnknownKeys' property to ignore unknown keys" ) return index -} \ No newline at end of file +} From 41706aa0a749d97fa3dca9df3f98a0db169447b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 17:14:40 +0100 Subject: [PATCH 38/68] Fix encodePositive KDoc --- .../commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt index caf777fa64..a0b3b59014 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt @@ -87,7 +87,7 @@ public interface CborEncoder : Encoder { public fun encodeNegative(value: ULong) /** - * Encode a positive value as [CborInt]. This function exists to encode negative values exceeding [Long.MAX_VALUE] + * Encode a positive value as [CborInt]. This function exists to encode positive values exceeding [Long.MAX_VALUE] */ public fun encodePositive(value: ULong) } From 20fef97f15a9fcd10797ecbd253c578bb9f2ab39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 17:21:38 +0100 Subject: [PATCH 39/68] Remove encodePositive/encodeNegative from CborEncoder --- .../src/kotlinx/serialization/cbor/CborEncoder.kt | 12 ------------ .../cbor/internal/CborElementSerializers.kt | 7 +------ .../serialization/cbor/internal/Encoder.kt | 15 +++++++-------- 3 files changed, 8 insertions(+), 26 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt index a0b3b59014..e6030dc9e2 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt @@ -6,8 +6,6 @@ package kotlinx.serialization.cbor import kotlinx.serialization.* import kotlinx.serialization.cbor.internal.CborElementSerializer -import kotlinx.serialization.cbor.internal.encodeNegative -import kotlinx.serialization.cbor.internal.encodePositive import kotlinx.serialization.encoding.* /** @@ -80,14 +78,4 @@ public interface CborEncoder : Encoder { * Encode CBOR `undefined` (simple value 23 / `0xF7`). */ public fun encodeUndefined() - - /** - * Encode a negative value as [CborInt]. This function exists to encode negative values exceeding [Long.MIN_VALUE] - */ - public fun encodeNegative(value: ULong) - - /** - * Encode a positive value as [CborInt]. This function exists to encode positive values exceeding [Long.MAX_VALUE] - */ - public fun encodePositive(value: ULong) } diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index 13ec0760a4..bc6757d53e 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -124,12 +124,7 @@ internal object CborIntSerializer : KSerializer, CborSerializer { override fun serialize(encoder: Encoder, value: CborInt) { val cborEncoder = encoder.asCborEncoder() cborEncoder.encodeTags(value.tags) - when (value.isPositive) { - //@formatter:off - true -> cborEncoder.encodePositive(value.value) - false -> cborEncoder.encodeNegative(value.value) - //@formatter:on - } + (cborEncoder as CborWriter).encodeCborInt(value.value, value.isPositive) } override fun deserialize(decoder: Decoder): CborInt { 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 52c7fc7506..68e60356d2 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -103,8 +103,11 @@ internal sealed class CborWriter( override fun encodeLong(value: Long) { getDestination().encodeNumber(value) } - override fun encodeNegative(value: ULong) = getDestination().encodeNegative(value) - override fun encodePositive(value: ULong) = getDestination().encodePositive(value) + + internal open fun encodeCborInt(absoluteValue: ULong, isPositive: Boolean) { + if (isPositive) getDestination().encodePositive(absoluteValue) + else getDestination().encodeNegative(absoluteValue) + } override fun encodeBoolean(value: Boolean) { getDestination().encodeBoolean(value) @@ -354,12 +357,8 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { currentElement += CborInt(value, tags = nextValueTags) } - override fun encodeNegative(value: ULong) { - currentElement += CborInt(value, isPositive = false, tags = nextValueTags) - } - - override fun encodePositive(value: ULong) { - currentElement += CborInt(value, isPositive = true, tags = nextValueTags) + override fun encodeCborInt(absoluteValue: ULong, isPositive: Boolean) { + currentElement += CborInt(absoluteValue, isPositive = isPositive, tags = nextValueTags) } From 50f9ca7784383c124b91568905b170ffe2f6f4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 17:22:45 +0100 Subject: [PATCH 40/68] Update CBOR API dumps --- formats/cbor/api/kotlinx-serialization-cbor.api | 2 -- formats/cbor/api/kotlinx-serialization-cbor.klib.api | 2 -- 2 files changed, 4 deletions(-) diff --git a/formats/cbor/api/kotlinx-serialization-cbor.api b/formats/cbor/api/kotlinx-serialization-cbor.api index 85d858cb1a..1aa1471e10 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.api @@ -116,8 +116,6 @@ public final class kotlinx/serialization/cbor/CborElement$Companion { public abstract interface class kotlinx/serialization/cbor/CborEncoder : kotlinx/serialization/encoding/Encoder { public abstract fun encodeByteString ([B)V public fun encodeCborElement (Lkotlinx/serialization/cbor/CborElement;)V - public abstract fun encodeNegative-VKZWuLQ (J)V - public abstract fun encodePositive-VKZWuLQ (J)V public abstract fun encodeTags-QwZRm1k ([J)V public abstract fun encodeUndefined ()V public abstract fun getCbor ()Lkotlinx/serialization/cbor/Cbor; diff --git a/formats/cbor/api/kotlinx-serialization-cbor.klib.api b/formats/cbor/api/kotlinx-serialization-cbor.klib.api index fafb29cad2..b65554a7f0 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.klib.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.klib.api @@ -58,8 +58,6 @@ abstract interface kotlinx.serialization.cbor/CborEncoder : kotlinx.serializatio abstract fun (): kotlinx.serialization.cbor/Cbor // kotlinx.serialization.cbor/CborEncoder.cbor.|(){}[0] abstract fun encodeByteString(kotlin/ByteArray) // kotlinx.serialization.cbor/CborEncoder.encodeByteString|encodeByteString(kotlin.ByteArray){}[0] - abstract fun encodeNegative(kotlin/ULong) // kotlinx.serialization.cbor/CborEncoder.encodeNegative|encodeNegative(kotlin.ULong){}[0] - abstract fun encodePositive(kotlin/ULong) // kotlinx.serialization.cbor/CborEncoder.encodePositive|encodePositive(kotlin.ULong){}[0] abstract fun encodeTags(kotlin/ULongArray) // kotlinx.serialization.cbor/CborEncoder.encodeTags|encodeTags(kotlin.ULongArray){}[0] abstract fun encodeUndefined() // kotlinx.serialization.cbor/CborEncoder.encodeUndefined|encodeUndefined(){}[0] open fun encodeCborElement(kotlinx.serialization.cbor/CborElement) // kotlinx.serialization.cbor/CborEncoder.encodeCborElement|encodeCborElement(kotlinx.serialization.cbor.CborElement){}[0] From ea8f75149622039b9dd30a57a6708d84ea77e5dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 17:24:57 +0100 Subject: [PATCH 41/68] Document and harden large integer encoding --- .../kotlinx/serialization/cbor/CborEncoder.kt | 3 +++ .../cbor/internal/CborElementSerializers.kt | 27 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt index e6030dc9e2..03e4fad945 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt @@ -24,6 +24,9 @@ import kotlinx.serialization.encoding.* * } * } * ``` + * + * To encode integers outside of the [Long] range, encode a [CborInt] explicitly + * (e.g. `encoder.encodeSerializableValue(CborInt.serializer(), CborInt(value = someULong, isPositive = true))`). */ @ExperimentalSerializationApi @SubclassOptInRequired(SealedSerializationApi::class) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index bc6757d53e..61c8860fce 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -124,7 +124,32 @@ internal object CborIntSerializer : KSerializer, CborSerializer { override fun serialize(encoder: Encoder, value: CborInt) { val cborEncoder = encoder.asCborEncoder() cborEncoder.encodeTags(value.tags) - (cborEncoder as CborWriter).encodeCborInt(value.value, value.isPositive) + when (cborEncoder) { + is CborWriter -> cborEncoder.encodeCborInt(value.value, value.isPositive) + else -> { + // Best-effort fallback for custom CborEncoder implementations. + if (value.isPositive) { + if (value.value <= Long.MAX_VALUE.toULong()) { + encoder.encodeLong(value.value.toLong()) + return + } + } else { + val abs = value.value + if (abs <= Long.MAX_VALUE.toULong()) { + encoder.encodeLong(-abs.toLong()) + return + } + if (abs == Long.MAX_VALUE.toULong() + 1uL) { + encoder.encodeLong(Long.MIN_VALUE) + return + } + } + throw IllegalStateException( + "This serializer can be used only with kotlinx.serialization.cbor's built-in encoder " + + "to encode full-range CBOR integers. Got ${encoder::class}." + ) + } + } } override fun deserialize(decoder: Decoder): CborInt { From 7e8d54d3a88cd50abd797e8e8dd64f7efd43fe55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 17:35:12 +0100 Subject: [PATCH 42/68] Restore encodePositive/encodeNegative API --- .../cbor/api/kotlinx-serialization-cbor.api | 2 ++ .../api/kotlinx-serialization-cbor.klib.api | 2 ++ .../kotlinx/serialization/cbor/CborEncoder.kt | 15 ++++++++-- .../cbor/internal/CborElementSerializers.kt | 30 ++++--------------- .../serialization/cbor/internal/Encoder.kt | 15 +++++----- 5 files changed, 29 insertions(+), 35 deletions(-) diff --git a/formats/cbor/api/kotlinx-serialization-cbor.api b/formats/cbor/api/kotlinx-serialization-cbor.api index 1aa1471e10..85d858cb1a 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.api @@ -116,6 +116,8 @@ public final class kotlinx/serialization/cbor/CborElement$Companion { public abstract interface class kotlinx/serialization/cbor/CborEncoder : kotlinx/serialization/encoding/Encoder { public abstract fun encodeByteString ([B)V public fun encodeCborElement (Lkotlinx/serialization/cbor/CborElement;)V + public abstract fun encodeNegative-VKZWuLQ (J)V + public abstract fun encodePositive-VKZWuLQ (J)V public abstract fun encodeTags-QwZRm1k ([J)V public abstract fun encodeUndefined ()V public abstract fun getCbor ()Lkotlinx/serialization/cbor/Cbor; diff --git a/formats/cbor/api/kotlinx-serialization-cbor.klib.api b/formats/cbor/api/kotlinx-serialization-cbor.klib.api index b65554a7f0..fafb29cad2 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.klib.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.klib.api @@ -58,6 +58,8 @@ abstract interface kotlinx.serialization.cbor/CborEncoder : kotlinx.serializatio abstract fun (): kotlinx.serialization.cbor/Cbor // kotlinx.serialization.cbor/CborEncoder.cbor.|(){}[0] abstract fun encodeByteString(kotlin/ByteArray) // kotlinx.serialization.cbor/CborEncoder.encodeByteString|encodeByteString(kotlin.ByteArray){}[0] + abstract fun encodeNegative(kotlin/ULong) // kotlinx.serialization.cbor/CborEncoder.encodeNegative|encodeNegative(kotlin.ULong){}[0] + abstract fun encodePositive(kotlin/ULong) // kotlinx.serialization.cbor/CborEncoder.encodePositive|encodePositive(kotlin.ULong){}[0] abstract fun encodeTags(kotlin/ULongArray) // kotlinx.serialization.cbor/CborEncoder.encodeTags|encodeTags(kotlin.ULongArray){}[0] abstract fun encodeUndefined() // kotlinx.serialization.cbor/CborEncoder.encodeUndefined|encodeUndefined(){}[0] open fun encodeCborElement(kotlinx.serialization.cbor/CborElement) // kotlinx.serialization.cbor/CborEncoder.encodeCborElement|encodeCborElement(kotlinx.serialization.cbor.CborElement){}[0] diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt index 03e4fad945..9c68c8966a 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt @@ -24,9 +24,6 @@ import kotlinx.serialization.encoding.* * } * } * ``` - * - * To encode integers outside of the [Long] range, encode a [CborInt] explicitly - * (e.g. `encoder.encodeSerializableValue(CborInt.serializer(), CborInt(value = someULong, isPositive = true))`). */ @ExperimentalSerializationApi @SubclassOptInRequired(SealedSerializationApi::class) @@ -81,4 +78,16 @@ public interface CborEncoder : Encoder { * Encode CBOR `undefined` (simple value 23 / `0xF7`). */ public fun encodeUndefined() + + /** + * Encode a negative integer (major type 1) with an absolute value that may exceed [Long.MIN_VALUE]. + * + * The encoded CBOR value is `-1 - (value - 1)` (i.e. `-value` in terms of [CborInt]'s absolute representation). + */ + public fun encodeNegative(value: ULong) + + /** + * Encode an unsigned integer (major type 0) that may exceed [Long.MAX_VALUE]. + */ + public fun encodePositive(value: ULong) } diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index 61c8860fce..ec83dfedf8 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -124,31 +124,11 @@ internal object CborIntSerializer : KSerializer, CborSerializer { override fun serialize(encoder: Encoder, value: CborInt) { val cborEncoder = encoder.asCborEncoder() cborEncoder.encodeTags(value.tags) - when (cborEncoder) { - is CborWriter -> cborEncoder.encodeCborInt(value.value, value.isPositive) - else -> { - // Best-effort fallback for custom CborEncoder implementations. - if (value.isPositive) { - if (value.value <= Long.MAX_VALUE.toULong()) { - encoder.encodeLong(value.value.toLong()) - return - } - } else { - val abs = value.value - if (abs <= Long.MAX_VALUE.toULong()) { - encoder.encodeLong(-abs.toLong()) - return - } - if (abs == Long.MAX_VALUE.toULong() + 1uL) { - encoder.encodeLong(Long.MIN_VALUE) - return - } - } - throw IllegalStateException( - "This serializer can be used only with kotlinx.serialization.cbor's built-in encoder " + - "to encode full-range CBOR integers. Got ${encoder::class}." - ) - } + when (value.isPositive) { + //@formatter:off + true -> cborEncoder.encodePositive(value.value) + false -> cborEncoder.encodeNegative(value.value) + //@formatter:on } } 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 68e60356d2..52c7fc7506 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -103,11 +103,8 @@ internal sealed class CborWriter( override fun encodeLong(value: Long) { getDestination().encodeNumber(value) } - - internal open fun encodeCborInt(absoluteValue: ULong, isPositive: Boolean) { - if (isPositive) getDestination().encodePositive(absoluteValue) - else getDestination().encodeNegative(absoluteValue) - } + override fun encodeNegative(value: ULong) = getDestination().encodeNegative(value) + override fun encodePositive(value: ULong) = getDestination().encodePositive(value) override fun encodeBoolean(value: Boolean) { getDestination().encodeBoolean(value) @@ -357,8 +354,12 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { currentElement += CborInt(value, tags = nextValueTags) } - override fun encodeCborInt(absoluteValue: ULong, isPositive: Boolean) { - currentElement += CborInt(absoluteValue, isPositive = isPositive, tags = nextValueTags) + override fun encodeNegative(value: ULong) { + currentElement += CborInt(value, isPositive = false, tags = nextValueTags) + } + + override fun encodePositive(value: ULong) { + currentElement += CborInt(value, isPositive = true, tags = nextValueTags) } From bf963eba269d0ea3ad7bc4dca53991f3a34faa7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 17:53:30 +0100 Subject: [PATCH 43/68] Make CborPrimitive non-generic --- .../cbor/api/kotlinx-serialization-cbor.api | 22 +++++-- .../api/kotlinx-serialization-cbor.klib.api | 61 ++++++++++++------- .../kotlinx/serialization/cbor/CborElement.kt | 34 ++++++----- .../cbor/internal/CborElementSerializers.kt | 10 +-- .../serialization/cbor/CborElementTest.kt | 2 +- 5 files changed, 84 insertions(+), 45 deletions(-) diff --git a/formats/cbor/api/kotlinx-serialization-cbor.api b/formats/cbor/api/kotlinx-serialization-cbor.api index 85d858cb1a..276f96253e 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.api @@ -30,6 +30,8 @@ public final synthetic class kotlinx/serialization/cbor/CborArray$Impl : kotlinx public final class kotlinx/serialization/cbor/CborBoolean : kotlinx/serialization/cbor/CborPrimitive { public static final field Companion Lkotlinx/serialization/cbor/CborBoolean$Companion; public synthetic fun (Z[JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getValue ()Ljava/lang/Boolean; + public synthetic fun getValue ()Ljava/lang/Object; } public final class kotlinx/serialization/cbor/CborBoolean$Companion { @@ -67,6 +69,8 @@ public final class kotlinx/serialization/cbor/CborByteString : kotlinx/serializa public static final field Companion Lkotlinx/serialization/cbor/CborByteString$Companion; public synthetic fun ([B[JLkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z + public synthetic fun getValue ()Ljava/lang/Object; + public fun getValue ()[B public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -134,6 +138,8 @@ public final class kotlinx/serialization/cbor/CborEncoder$DefaultImpls { public final class kotlinx/serialization/cbor/CborFloat : kotlinx/serialization/cbor/CborPrimitive { public static final field Companion Lkotlinx/serialization/cbor/CborFloat$Companion; public synthetic fun (D[JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getValue ()Ljava/lang/Double; + public synthetic fun getValue ()Ljava/lang/Object; } public final class kotlinx/serialization/cbor/CborFloat$Companion { @@ -144,6 +150,8 @@ public final class kotlinx/serialization/cbor/CborInt : kotlinx/serialization/cb public static final field Companion Lkotlinx/serialization/cbor/CborInt$Companion; public synthetic fun (JZ[JLkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z + public synthetic fun getValue ()Ljava/lang/Object; + public fun getValue-s-VKNKU ()J public fun hashCode ()I public final fun isPositive ()Z public final fun toLong ()J @@ -275,6 +283,8 @@ public final class kotlinx/serialization/cbor/CborMap$Companion { public final class kotlinx/serialization/cbor/CborNull : kotlinx/serialization/cbor/CborPrimitive { public static final field Companion Lkotlinx/serialization/cbor/CborNull$Companion; public synthetic fun ([JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun getValue ()Ljava/lang/Object; + public fun getValue ()Lkotlin/Unit; } public final class kotlinx/serialization/cbor/CborNull$Companion { @@ -290,21 +300,23 @@ public final synthetic class kotlinx/serialization/cbor/CborNullAsEmptyMap$Impl public abstract class kotlinx/serialization/cbor/CborPrimitive : kotlinx/serialization/cbor/CborElement { public static final field Companion Lkotlinx/serialization/cbor/CborPrimitive$Companion; - public synthetic fun (Ljava/lang/Object;[JILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Ljava/lang/Object;[JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun ([JILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun ([JLkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z - public final fun getValue ()Ljava/lang/Object; + protected abstract fun getValue ()Ljava/lang/Object; public fun hashCode ()I public fun toString ()Ljava/lang/String; } public final class kotlinx/serialization/cbor/CborPrimitive$Companion { - public final fun serializer (Lkotlinx/serialization/KSerializer;)Lkotlinx/serialization/KSerializer; + public final fun serializer ()Lkotlinx/serialization/KSerializer; } public final class kotlinx/serialization/cbor/CborString : kotlinx/serialization/cbor/CborPrimitive { public static final field Companion Lkotlinx/serialization/cbor/CborString$Companion; public synthetic fun (Ljava/lang/String;[JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun getValue ()Ljava/lang/Object; + public fun getValue ()Ljava/lang/String; } public final class kotlinx/serialization/cbor/CborString$Companion { @@ -335,6 +347,8 @@ public final class kotlinx/serialization/cbor/CborTag { public final class kotlinx/serialization/cbor/CborUndefined : kotlinx/serialization/cbor/CborPrimitive { public static final field Companion Lkotlinx/serialization/cbor/CborUndefined$Companion; public synthetic fun ([JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun getValue ()Ljava/lang/Object; + public fun getValue ()Lkotlin/Unit; } public final class kotlinx/serialization/cbor/CborUndefined$Companion { diff --git a/formats/cbor/api/kotlinx-serialization-cbor.klib.api b/formats/cbor/api/kotlinx-serialization-cbor.klib.api index fafb29cad2..56af81ddad 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.klib.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.klib.api @@ -65,9 +65,12 @@ abstract interface kotlinx.serialization.cbor/CborEncoder : kotlinx.serializatio open fun encodeCborElement(kotlinx.serialization.cbor/CborElement) // kotlinx.serialization.cbor/CborEncoder.encodeCborElement|encodeCborElement(kotlinx.serialization.cbor.CborElement){}[0] } -final class kotlinx.serialization.cbor/CborBoolean : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborBoolean|null[0] +final class kotlinx.serialization.cbor/CborBoolean : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborBoolean|null[0] constructor (kotlin/Boolean, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborBoolean.|(kotlin.Boolean;kotlin.ULongArray...){}[0] + final val value // kotlinx.serialization.cbor/CborBoolean.value|{}value[0] + final fun (): kotlin/Boolean // kotlinx.serialization.cbor/CborBoolean.value.|(){}[0] + final object Companion { // kotlinx.serialization.cbor/CborBoolean.Companion|null[0] final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborBoolean.Companion.serializer|serializer(){}[0] } @@ -112,9 +115,12 @@ final class kotlinx.serialization.cbor/CborBuilder { // kotlinx.serialization.cb final fun (kotlin/Boolean) // kotlinx.serialization.cbor/CborBuilder.verifyValueTags.|(kotlin.Boolean){}[0] } -final class kotlinx.serialization.cbor/CborByteString : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborByteString|null[0] +final class kotlinx.serialization.cbor/CborByteString : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborByteString|null[0] constructor (kotlin/ByteArray, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborByteString.|(kotlin.ByteArray;kotlin.ULongArray...){}[0] + final val value // kotlinx.serialization.cbor/CborByteString.value|{}value[0] + final fun (): kotlin/ByteArray // kotlinx.serialization.cbor/CborByteString.value.|(){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborByteString.equals|equals(kotlin.Any?){}[0] final fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborByteString.hashCode|hashCode(){}[0] final fun toString(): kotlin/String // kotlinx.serialization.cbor/CborByteString.toString|toString(){}[0] @@ -151,19 +157,24 @@ final class kotlinx.serialization.cbor/CborConfiguration { // kotlinx.serializat final fun toString(): kotlin/String // kotlinx.serialization.cbor/CborConfiguration.toString|toString(){}[0] } -final class kotlinx.serialization.cbor/CborFloat : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborFloat|null[0] +final class kotlinx.serialization.cbor/CborFloat : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborFloat|null[0] constructor (kotlin/Double, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborFloat.|(kotlin.Double;kotlin.ULongArray...){}[0] + final val value // kotlinx.serialization.cbor/CborFloat.value|{}value[0] + final fun (): kotlin/Double // kotlinx.serialization.cbor/CborFloat.value.|(){}[0] + final object Companion { // kotlinx.serialization.cbor/CborFloat.Companion|null[0] final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborFloat.Companion.serializer|serializer(){}[0] } } -final class kotlinx.serialization.cbor/CborInt : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborInt|null[0] +final class kotlinx.serialization.cbor/CborInt : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborInt|null[0] constructor (kotlin/ULong, kotlin/Boolean, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborInt.|(kotlin.ULong;kotlin.Boolean;kotlin.ULongArray...){}[0] final val isPositive // kotlinx.serialization.cbor/CborInt.isPositive|{}isPositive[0] final fun (): kotlin/Boolean // kotlinx.serialization.cbor/CborInt.isPositive.|(){}[0] + final val value // kotlinx.serialization.cbor/CborInt.value|{}value[0] + final fun (): kotlin/ULong // kotlinx.serialization.cbor/CborInt.value.|(){}[0] final fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborInt.equals|equals(kotlin.Any?){}[0] final fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborInt.hashCode|hashCode(){}[0] @@ -239,44 +250,39 @@ final class kotlinx.serialization.cbor/CborMap : kotlin.collections/Map // kotlinx.serialization.cbor/CborMap.asJsReadonlyMapView|asJsReadonlyMapView(){}[0] } -final class kotlinx.serialization.cbor/CborNull : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborNull|null[0] +final class kotlinx.serialization.cbor/CborNull : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborNull|null[0] constructor (kotlin/ULongArray...) // kotlinx.serialization.cbor/CborNull.|(kotlin.ULongArray...){}[0] + final val value // kotlinx.serialization.cbor/CborNull.value|{}value[0] + final fun () // kotlinx.serialization.cbor/CborNull.value.|(){}[0] + final object Companion { // kotlinx.serialization.cbor/CborNull.Companion|null[0] final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborNull.Companion.serializer|serializer(){}[0] } } -final class kotlinx.serialization.cbor/CborString : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborString|null[0] +final class kotlinx.serialization.cbor/CborString : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborString|null[0] constructor (kotlin/String, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborString.|(kotlin.String;kotlin.ULongArray...){}[0] + final val value // kotlinx.serialization.cbor/CborString.value|{}value[0] + final fun (): kotlin/String // kotlinx.serialization.cbor/CborString.value.|(){}[0] + final object Companion { // kotlinx.serialization.cbor/CborString.Companion|null[0] final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborString.Companion.serializer|serializer(){}[0] } } -final class kotlinx.serialization.cbor/CborUndefined : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborUndefined|null[0] +final class kotlinx.serialization.cbor/CborUndefined : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborUndefined|null[0] constructor (kotlin/ULongArray...) // kotlinx.serialization.cbor/CborUndefined.|(kotlin.ULongArray...){}[0] + final val value // kotlinx.serialization.cbor/CborUndefined.value|{}value[0] + final fun () // kotlinx.serialization.cbor/CborUndefined.value.|(){}[0] + final object Companion { // kotlinx.serialization.cbor/CborUndefined.Companion|null[0] final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborUndefined.Companion.serializer|serializer(){}[0] } } -sealed class <#A: kotlin/Any> kotlinx.serialization.cbor/CborPrimitive : kotlinx.serialization.cbor/CborElement { // kotlinx.serialization.cbor/CborPrimitive|null[0] - final val value // kotlinx.serialization.cbor/CborPrimitive.value|{}value[0] - final fun (): #A // kotlinx.serialization.cbor/CborPrimitive.value.|(){}[0] - - open fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborPrimitive.equals|equals(kotlin.Any?){}[0] - open fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborPrimitive.hashCode|hashCode(){}[0] - open fun toString(): kotlin/String // kotlinx.serialization.cbor/CborPrimitive.toString|toString(){}[0] - - final object Companion : kotlinx.serialization.internal/SerializerFactory { // kotlinx.serialization.cbor/CborPrimitive.Companion|null[0] - final fun <#A2: kotlin/Any?> serializer(kotlinx.serialization/KSerializer<#A2>): kotlinx.serialization/KSerializer> // kotlinx.serialization.cbor/CborPrimitive.Companion.serializer|serializer(kotlinx.serialization.KSerializer<0:0>){0§}[0] - final fun serializer(kotlin/Array>...): kotlinx.serialization/KSerializer<*> // kotlinx.serialization.cbor/CborPrimitive.Companion.serializer|serializer(kotlin.Array>...){}[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] @@ -306,6 +312,19 @@ sealed class kotlinx.serialization.cbor/CborElement { // kotlinx.serialization.c } } +sealed class kotlinx.serialization.cbor/CborPrimitive : kotlinx.serialization.cbor/CborElement { // kotlinx.serialization.cbor/CborPrimitive|null[0] + abstract val value // kotlinx.serialization.cbor/CborPrimitive.value|{}value[0] + abstract fun (): kotlin/Any // kotlinx.serialization.cbor/CborPrimitive.value.|(){}[0] + + open fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborPrimitive.equals|equals(kotlin.Any?){}[0] + open fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborPrimitive.hashCode|hashCode(){}[0] + open fun toString(): kotlin/String // kotlinx.serialization.cbor/CborPrimitive.toString|toString(){}[0] + + final object Companion { // kotlinx.serialization.cbor/CborPrimitive.Companion|null[0] + final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborPrimitive.Companion.serializer|serializer(){}[0] + } +} + final object kotlinx.serialization.cbor/CborTag { // kotlinx.serialization.cbor/CborTag|null[0] final const val BASE16 // kotlinx.serialization.cbor/CborTag.BASE16|{}BASE16[0] final fun (): kotlin/ULong // kotlinx.serialization.cbor/CborTag.BASE16.|(){}[0] diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt index 3c3a9469c8..42922476d0 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -65,13 +65,14 @@ public sealed class CborElement( */ @Serializable(with = CborPrimitiveSerializer::class) @ExperimentalSerializationApi -public sealed class CborPrimitive( - public val value: T, +public sealed class CborPrimitive( tags: ULongArray = EMPTY_TAGS ) : CborElement(tags) { + protected abstract val value: Any + override fun equals(other: Any?): Boolean { if (this === other) return true - if (other !is CborPrimitive<*>) return false + if (other !is CborPrimitive) return false if (!super.equals(other)) return false if (value != other.value) return false @@ -106,7 +107,8 @@ public class CborInt( absoluteValue: ULong, public val isPositive: Boolean, vararg tags: ULong -) : CborPrimitive(absoluteValue, tags) { +) : CborPrimitive(tags) { + public override val value: ULong = absoluteValue init { if (!isPositive) require(absoluteValue > 0uL) { "Illegal absolute value $absoluteValue for a negative number." } @@ -176,9 +178,9 @@ public class CborInt( @Serializable(with = CborFloatSerializer::class) @ExperimentalSerializationApi public class CborFloat( - value: Double, + public override val value: Double, vararg tags: ULong -) : CborPrimitive(value, tags) +) : CborPrimitive(tags) /** * Class representing CBOR string value. @@ -186,9 +188,9 @@ public class CborFloat( @Serializable(with = CborStringSerializer::class) @ExperimentalSerializationApi public class CborString( - value: String, + public override val value: String, vararg tags: ULong -) : CborPrimitive(value, tags) +) : CborPrimitive(tags) /** * Class representing CBOR boolean value. @@ -196,9 +198,9 @@ public class CborString( @Serializable(with = CborBooleanSerializer::class) @ExperimentalSerializationApi public class CborBoolean( - value: Boolean, + public override val value: Boolean, vararg tags: ULong -) : CborPrimitive(value, tags) +) : CborPrimitive(tags) /** * Class representing CBOR byte string value. @@ -206,9 +208,9 @@ public class CborBoolean( @Serializable(with = CborByteStringSerializer::class) @ExperimentalSerializationApi public class CborByteString( - value: ByteArray, + public override val value: ByteArray, vararg tags: ULong -) : CborPrimitive(value, tags) { +) : CborPrimitive(tags) { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is CborByteString) return false @@ -235,14 +237,18 @@ public class CborByteString( */ @Serializable(with = CborNullSerializer::class) @ExperimentalSerializationApi -public class CborNull(vararg tags: ULong) : CborPrimitive(Unit, tags) +public class CborNull(vararg tags: ULong) : CborPrimitive(tags) { + public override val value: Unit = Unit +} /** * Class representing CBOR `undefined` value */ @Serializable(with = CborUndefinedSerializer::class) @ExperimentalSerializationApi -public class CborUndefined(vararg tags: ULong) : CborPrimitive(Unit, tags) +public class CborUndefined(vararg tags: ULong) : CborPrimitive(tags) { + public override val value: Unit = Unit +} /** * Class representing CBOR map, consisting of key-value pairs, where both key and value are arbitrary [CborElement] diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index ec83dfedf8..d51c7ef54a 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -38,7 +38,7 @@ internal object CborElementSerializer : KSerializer, CborSerializer // Encode the value when (value) { - is CborPrimitive<*> -> encoder.encodeSerializableValue(CborPrimitiveSerializer, value) + is CborPrimitive -> encoder.encodeSerializableValue(CborPrimitiveSerializer, value) is CborMap -> encoder.encodeSerializableValue(CborMapSerializer, value) is CborList -> encoder.encodeSerializableValue(CborListSerializer, value) } @@ -54,11 +54,11 @@ internal object CborElementSerializer : KSerializer, CborSerializer * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborPrimitive]. * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). */ -internal object CborPrimitiveSerializer : KSerializer>, CborSerializer { +internal object CborPrimitiveSerializer : KSerializer, CborSerializer { override val descriptor: SerialDescriptor = buildSerialDescriptor("kotlinx.serialization.cbor.CborPrimitive", PolymorphicKind.SEALED) - override fun serialize(encoder: Encoder, value: CborPrimitive<*>) { + override fun serialize(encoder: Encoder, value: CborPrimitive) { when (value) { is CborNull -> encoder.encodeSerializableValue(CborNullSerializer, value) is CborUndefined -> encoder.encodeSerializableValue(CborUndefinedSerializer, value) @@ -70,9 +70,9 @@ internal object CborPrimitiveSerializer : KSerializer>, CborSer } } - override fun deserialize(decoder: Decoder): CborPrimitive<*> { + override fun deserialize(decoder: Decoder): CborPrimitive { val result = decoder.asCborDecoder().decodeCborElement() - if (result !is CborPrimitive<*>) throw CborDecodingException("Unexpected CBOR element, expected CborPrimitive, had ${result::class}") + if (result !is CborPrimitive) throw CborDecodingException("Unexpected CBOR element, expected CborPrimitive, had ${result::class}") return result } } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index ccc48d4c2a..fb6cb9000a 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -751,7 +751,7 @@ data class MixedBag( val str: String, val bStr: CborByteString?, val cborElement: CborElement?, - val cborPositiveInt: CborPrimitive<*>, + val cborPositiveInt: CborPrimitive, val cborInt: CborInt, @KeyTags(42u) @ValueTags(2337u) From 4fabf812b71f3ea84568b3d084dd40f7d827e162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 18:36:14 +0100 Subject: [PATCH 44/68] Add test for encodeCborElement misuse --- .../cbor/CborEncoderMisuseTest.kt | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborEncoderMisuseTest.kt diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborEncoderMisuseTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborEncoderMisuseTest.kt new file mode 100644 index 0000000000..adc4e09a43 --- /dev/null +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborEncoderMisuseTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +@file:OptIn(ExperimentalSerializationApi::class, ExperimentalUnsignedTypes::class) + +package kotlinx.serialization.cbor + +import kotlinx.serialization.* +import kotlinx.serialization.cbor.internal.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* +import kotlin.test.* + +class CborEncoderMisuseTest { + + @Serializable(with = BadInStructureElementWrite.Serializer::class) + private data class BadInStructureElementWrite(val x: Int) { + object Serializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("BadInStructureElementWrite") { + element("x") + } + + override fun serialize(encoder: Encoder, value: BadInStructureElementWrite) { + val composite = encoder.beginStructure(descriptor) + composite.encodeIntElement(descriptor, 0, value.x) + + // Illegal usage: inject an element without going through encodeElement(descriptor, index). + (composite as CborEncoder).encodeCborElement(CborString("oops")) + + composite.endStructure(descriptor) + } + + override fun deserialize(decoder: Decoder): BadInStructureElementWrite { + val composite = decoder.beginStructure(descriptor) + var x: Int? = null + while (true) { + when (val index = composite.decodeElementIndex(descriptor)) { + CompositeDecoder.DECODE_DONE -> break + 0 -> x = composite.decodeIntElement(descriptor, 0) + else -> throw SerializationException("Unexpected index $index") + } + } + composite.endStructure(descriptor) + return BadInStructureElementWrite(x ?: throw SerializationException("Missing x")) + } + } + } + + @Test + fun testEncodeCborElementInsideStructureProducesMultipleRootItemsDefiniteLength() { + val cbor = Cbor { useDefiniteLengthEncoding = true } + + val bytes = cbor.encodeToByteArray(BadInStructureElementWrite(1)) + assertEquals("a1617801646f6f7073", bytes.toHexString()) + + val parser = CborParser(ByteArrayInput(bytes), verifyObjectTags = false) + val reader = CborTreeReader(cbor.configuration, parser) + + val first = reader.read() + assertEquals(CborMap(mapOf(CborString("x") to CborInt(1))), first) + assertFalse(parser.isEof(), "Expected trailing bytes (second root item) after the first CBOR item") + + val second = reader.read() + assertEquals(CborString("oops"), second) + assertTrue(parser.isEof(), "Expected exactly two root CBOR items") + } +} + From c4390f8307785c6a0f078c570fa2fcf5107d7383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 19:00:07 +0100 Subject: [PATCH 45/68] Revert "Add test for encodeCborElement misuse" This reverts commit 847be408efb039714a5130a93f8c1cbb890f24d2. --- .../cbor/CborEncoderMisuseTest.kt | 69 ------------------- 1 file changed, 69 deletions(-) delete mode 100644 formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborEncoderMisuseTest.kt diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborEncoderMisuseTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborEncoderMisuseTest.kt deleted file mode 100644 index adc4e09a43..0000000000 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborEncoderMisuseTest.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ -@file:OptIn(ExperimentalSerializationApi::class, ExperimentalUnsignedTypes::class) - -package kotlinx.serialization.cbor - -import kotlinx.serialization.* -import kotlinx.serialization.cbor.internal.* -import kotlinx.serialization.descriptors.* -import kotlinx.serialization.encoding.* -import kotlin.test.* - -class CborEncoderMisuseTest { - - @Serializable(with = BadInStructureElementWrite.Serializer::class) - private data class BadInStructureElementWrite(val x: Int) { - object Serializer : KSerializer { - override val descriptor: SerialDescriptor = - buildClassSerialDescriptor("BadInStructureElementWrite") { - element("x") - } - - override fun serialize(encoder: Encoder, value: BadInStructureElementWrite) { - val composite = encoder.beginStructure(descriptor) - composite.encodeIntElement(descriptor, 0, value.x) - - // Illegal usage: inject an element without going through encodeElement(descriptor, index). - (composite as CborEncoder).encodeCborElement(CborString("oops")) - - composite.endStructure(descriptor) - } - - override fun deserialize(decoder: Decoder): BadInStructureElementWrite { - val composite = decoder.beginStructure(descriptor) - var x: Int? = null - while (true) { - when (val index = composite.decodeElementIndex(descriptor)) { - CompositeDecoder.DECODE_DONE -> break - 0 -> x = composite.decodeIntElement(descriptor, 0) - else -> throw SerializationException("Unexpected index $index") - } - } - composite.endStructure(descriptor) - return BadInStructureElementWrite(x ?: throw SerializationException("Missing x")) - } - } - } - - @Test - fun testEncodeCborElementInsideStructureProducesMultipleRootItemsDefiniteLength() { - val cbor = Cbor { useDefiniteLengthEncoding = true } - - val bytes = cbor.encodeToByteArray(BadInStructureElementWrite(1)) - assertEquals("a1617801646f6f7073", bytes.toHexString()) - - val parser = CborParser(ByteArrayInput(bytes), verifyObjectTags = false) - val reader = CborTreeReader(cbor.configuration, parser) - - val first = reader.read() - assertEquals(CborMap(mapOf(CborString("x") to CborInt(1))), first) - assertFalse(parser.isEof(), "Expected trailing bytes (second root item) after the first CBOR item") - - val second = reader.read() - assertEquals(CborString("oops"), second) - assertTrue(parser.isEof(), "Expected exactly two root CBOR items") - } -} - From e29b6f366234200dd4df4f9b076a12d684c949b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 19:05:29 +0100 Subject: [PATCH 46/68] Add top-level CborInt factory functions --- .../cbor/api/kotlinx-serialization-cbor.api | 7 ++- .../api/kotlinx-serialization-cbor.klib.api | 4 +- .../kotlinx/serialization/cbor/CborElement.kt | 48 +++++++++---------- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/formats/cbor/api/kotlinx-serialization-cbor.api b/formats/cbor/api/kotlinx-serialization-cbor.api index 276f96253e..f43ef12fdd 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.api @@ -117,6 +117,11 @@ public final class kotlinx/serialization/cbor/CborElement$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class kotlinx/serialization/cbor/CborElementKt { + public static final fun CborInt-SIFponk (J[J)Lkotlinx/serialization/cbor/CborInt; + public static final fun CborInt-ahITK_k (J[J)Lkotlinx/serialization/cbor/CborInt; +} + public abstract interface class kotlinx/serialization/cbor/CborEncoder : kotlinx/serialization/encoding/Encoder { public abstract fun encodeByteString ([B)V public fun encodeCborElement (Lkotlinx/serialization/cbor/CborElement;)V @@ -159,8 +164,6 @@ public final class kotlinx/serialization/cbor/CborInt : kotlinx/serialization/cb } public final class kotlinx/serialization/cbor/CborInt$Companion { - public final fun invoke-SIFponk (J[J)Lkotlinx/serialization/cbor/CborInt; - public final fun invoke-ahITK_k (J[J)Lkotlinx/serialization/cbor/CborInt; public final fun serializer ()Lkotlinx/serialization/KSerializer; } diff --git a/formats/cbor/api/kotlinx-serialization-cbor.klib.api b/formats/cbor/api/kotlinx-serialization-cbor.klib.api index 56af81ddad..50eeaceffb 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.klib.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.klib.api @@ -182,8 +182,6 @@ final class kotlinx.serialization.cbor/CborInt : kotlinx.serialization.cbor/Cbor final fun toString(): kotlin/String // kotlinx.serialization.cbor/CborInt.toString|toString(){}[0] final object Companion { // kotlinx.serialization.cbor/CborInt.Companion|null[0] - final fun invoke(kotlin/Long, kotlin/ULongArray...): kotlinx.serialization.cbor/CborInt // kotlinx.serialization.cbor/CborInt.Companion.invoke|invoke(kotlin.Long;kotlin.ULongArray...){}[0] - final fun invoke(kotlin/ULong, kotlin/ULongArray...): kotlinx.serialization.cbor/CborInt // kotlinx.serialization.cbor/CborInt.Companion.invoke|invoke(kotlin.ULong;kotlin.ULongArray...){}[0] final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborInt.Companion.serializer|serializer(){}[0] } } @@ -363,5 +361,7 @@ final object kotlinx.serialization.cbor/CborTag { // kotlinx.serialization.cbor/ } final fun kotlinx.serialization.cbor/Cbor(kotlinx.serialization.cbor/Cbor = ..., kotlin/Function1): kotlinx.serialization.cbor/Cbor // kotlinx.serialization.cbor/Cbor|Cbor(kotlinx.serialization.cbor.Cbor;kotlin.Function1){}[0] +final fun kotlinx.serialization.cbor/CborInt(kotlin/Long, kotlin/ULongArray...): kotlinx.serialization.cbor/CborInt // kotlinx.serialization.cbor/CborInt|CborInt(kotlin.Long;kotlin.ULongArray...){}[0] +final fun kotlinx.serialization.cbor/CborInt(kotlin/ULong, kotlin/ULongArray...): kotlinx.serialization.cbor/CborInt // kotlinx.serialization.cbor/CborInt|CborInt(kotlin.ULong;kotlin.ULongArray...){}[0] final inline fun <#A: reified kotlin/Any?> (kotlinx.serialization.cbor/Cbor).kotlinx.serialization.cbor/decodeFromCborElement(kotlinx.serialization.cbor/CborElement): #A // kotlinx.serialization.cbor/decodeFromCborElement|decodeFromCborElement@kotlinx.serialization.cbor.Cbor(kotlinx.serialization.cbor.CborElement){0§}[0] final inline fun <#A: reified kotlin/Any?> (kotlinx.serialization.cbor/Cbor).kotlinx.serialization.cbor/encodeToCborElement(#A): kotlinx.serialization.cbor/CborElement // kotlinx.serialization.cbor/encodeToCborElement|encodeToCborElement@kotlinx.serialization.cbor.Cbor(0:0){0§}[0] diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt index 42922476d0..be4479be45 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -119,32 +119,6 @@ public class CborInt( */ public fun toLong(): Long = if (isPositive) value.toLong() else -value.toLong() - - public companion object { - /** - * Creates: - * * signed CBOR integer (major type 1 encompassing `-2^64..-1`) - * * unsigned CBOR integer (major type 0 encompassing `0..2^64-1`) - * - * depending on whether a positive or a negative number was passed. - * If you want to create a negative number exceeding [Long.MIN_VALUE], manually specify sign: `CborInt(ULong.MAX_VALUE, isPositive = false)` - */ - public operator fun invoke( - value: Long, - vararg tags: ULong - ): CborInt = - if (value >= 0L) CborInt(value.toULong(), isPositive = true, tags = tags) - else CborInt(ULong.MAX_VALUE - value.toULong() + 1uL, isPositive = false, tags = tags) - - /** - * Creates an unsigned CBOR integer (major type 0). - */ - public operator fun invoke( - value: ULong, - vararg tags: ULong - ): CborInt = CborInt(value, isPositive = true, tags = tags) - } - override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is CborInt) return false @@ -172,6 +146,28 @@ public class CborInt( } } +/** + * Creates: + * * signed CBOR integer (major type 1 encompassing `-2^64..-1`) + * * unsigned CBOR integer (major type 0 encompassing `0..2^64-1`) + * + * depending on whether a positive or a negative number was passed. + * If you want to create a negative number exceeding [Long.MIN_VALUE], manually specify sign: `CborInt(ULong.MAX_VALUE, isPositive = false)`. + */ +@ExperimentalSerializationApi +@Suppress("FunctionName") +public fun CborInt(value: Long, vararg tags: ULong): CborInt = + if (value >= 0L) CborInt(value.toULong(), isPositive = true, tags = tags) + else CborInt(ULong.MAX_VALUE - value.toULong() + 1uL, isPositive = false, tags = tags) + +/** + * Creates an unsigned CBOR integer (major type 0). + */ +@ExperimentalSerializationApi +@Suppress("FunctionName") +public fun CborInt(value: ULong, vararg tags: ULong): CborInt = + CborInt(value, isPositive = true, tags = tags) + /** * Class representing CBOR floating point value (major type 7). */ From 2f7bbb465e6a17c86e3ec7a15ecdc8b378d80224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 19:15:08 +0100 Subject: [PATCH 47/68] Add safe CborInt numeric conversions --- .../cbor/api/kotlinx-serialization-cbor.api | 8 ++ .../api/kotlinx-serialization-cbor.klib.api | 17 ++++ .../kotlinx/serialization/cbor/CborElement.kt | 84 +++++++++++++++++-- .../serialization/cbor/internal/Decoder.kt | 16 ++-- .../serialization/cbor/CborElementTest.kt | 13 ++- 5 files changed, 117 insertions(+), 21 deletions(-) diff --git a/formats/cbor/api/kotlinx-serialization-cbor.api b/formats/cbor/api/kotlinx-serialization-cbor.api index f43ef12fdd..15373025c7 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.api @@ -120,6 +120,14 @@ public final class kotlinx/serialization/cbor/CborElement$Companion { public final class kotlinx/serialization/cbor/CborElementKt { public static final fun CborInt-SIFponk (J[J)Lkotlinx/serialization/cbor/CborInt; public static final fun CborInt-ahITK_k (J[J)Lkotlinx/serialization/cbor/CborInt; + public static final fun getByte (Lkotlinx/serialization/cbor/CborInt;)B + public static final fun getByteOrNull (Lkotlinx/serialization/cbor/CborInt;)Ljava/lang/Byte; + public static final fun getInt (Lkotlinx/serialization/cbor/CborInt;)I + public static final fun getIntOrNull (Lkotlinx/serialization/cbor/CborInt;)Ljava/lang/Integer; + public static final fun getLong (Lkotlinx/serialization/cbor/CborInt;)J + public static final fun getLongOrNull (Lkotlinx/serialization/cbor/CborInt;)Ljava/lang/Long; + public static final fun getShort (Lkotlinx/serialization/cbor/CborInt;)S + public static final fun getShortOrNull (Lkotlinx/serialization/cbor/CborInt;)Ljava/lang/Short; } public abstract interface class kotlinx/serialization/cbor/CborEncoder : kotlinx/serialization/encoding/Encoder { diff --git a/formats/cbor/api/kotlinx-serialization-cbor.klib.api b/formats/cbor/api/kotlinx-serialization-cbor.klib.api index 50eeaceffb..eb198d2495 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.klib.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.klib.api @@ -360,6 +360,23 @@ final object kotlinx.serialization.cbor/CborTag { // kotlinx.serialization.cbor/ final fun (): kotlin/ULong // kotlinx.serialization.cbor/CborTag.URI.|(){}[0] } +final val kotlinx.serialization.cbor/byte // kotlinx.serialization.cbor/byte|@kotlinx.serialization.cbor.CborInt{}byte[0] + final fun (kotlinx.serialization.cbor/CborInt).(): kotlin/Byte // kotlinx.serialization.cbor/byte.|@kotlinx.serialization.cbor.CborInt(){}[0] +final val kotlinx.serialization.cbor/byteOrNull // kotlinx.serialization.cbor/byteOrNull|@kotlinx.serialization.cbor.CborInt{}byteOrNull[0] + final fun (kotlinx.serialization.cbor/CborInt).(): kotlin/Byte? // kotlinx.serialization.cbor/byteOrNull.|@kotlinx.serialization.cbor.CborInt(){}[0] +final val kotlinx.serialization.cbor/int // kotlinx.serialization.cbor/int|@kotlinx.serialization.cbor.CborInt{}int[0] + final fun (kotlinx.serialization.cbor/CborInt).(): kotlin/Int // kotlinx.serialization.cbor/int.|@kotlinx.serialization.cbor.CborInt(){}[0] +final val kotlinx.serialization.cbor/intOrNull // kotlinx.serialization.cbor/intOrNull|@kotlinx.serialization.cbor.CborInt{}intOrNull[0] + final fun (kotlinx.serialization.cbor/CborInt).(): kotlin/Int? // kotlinx.serialization.cbor/intOrNull.|@kotlinx.serialization.cbor.CborInt(){}[0] +final val kotlinx.serialization.cbor/long // kotlinx.serialization.cbor/long|@kotlinx.serialization.cbor.CborInt{}long[0] + final fun (kotlinx.serialization.cbor/CborInt).(): kotlin/Long // kotlinx.serialization.cbor/long.|@kotlinx.serialization.cbor.CborInt(){}[0] +final val kotlinx.serialization.cbor/longOrNull // kotlinx.serialization.cbor/longOrNull|@kotlinx.serialization.cbor.CborInt{}longOrNull[0] + final fun (kotlinx.serialization.cbor/CborInt).(): kotlin/Long? // kotlinx.serialization.cbor/longOrNull.|@kotlinx.serialization.cbor.CborInt(){}[0] +final val kotlinx.serialization.cbor/short // kotlinx.serialization.cbor/short|@kotlinx.serialization.cbor.CborInt{}short[0] + final fun (kotlinx.serialization.cbor/CborInt).(): kotlin/Short // kotlinx.serialization.cbor/short.|@kotlinx.serialization.cbor.CborInt(){}[0] +final val kotlinx.serialization.cbor/shortOrNull // kotlinx.serialization.cbor/shortOrNull|@kotlinx.serialization.cbor.CborInt{}shortOrNull[0] + final fun (kotlinx.serialization.cbor/CborInt).(): kotlin/Short? // kotlinx.serialization.cbor/shortOrNull.|@kotlinx.serialization.cbor.CborInt(){}[0] + final fun kotlinx.serialization.cbor/Cbor(kotlinx.serialization.cbor/Cbor = ..., kotlin/Function1): kotlinx.serialization.cbor/Cbor // kotlinx.serialization.cbor/Cbor|Cbor(kotlinx.serialization.cbor.Cbor;kotlin.Function1){}[0] final fun kotlinx.serialization.cbor/CborInt(kotlin/Long, kotlin/ULongArray...): kotlinx.serialization.cbor/CborInt // kotlinx.serialization.cbor/CborInt|CborInt(kotlin.Long;kotlin.ULongArray...){}[0] final fun kotlinx.serialization.cbor/CborInt(kotlin/ULong, kotlin/ULongArray...): kotlinx.serialization.cbor/CborInt // kotlinx.serialization.cbor/CborInt|CborInt(kotlin.ULong;kotlin.ULongArray...){}[0] diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt index be4479be45..73e878048b 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -114,11 +114,6 @@ public class CborInt( if (!isPositive) require(absoluteValue > 0uL) { "Illegal absolute value $absoluteValue for a negative number." } } - /** - * **WARNING! Possible truncation/overflow!** E.g., `-2^64` -> `1` - */ - public fun toLong(): Long = if (isPositive) value.toLong() else -value.toLong() - override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is CborInt) return false @@ -168,6 +163,85 @@ public fun CborInt(value: Long, vararg tags: ULong): CborInt = public fun CborInt(value: ULong, vararg tags: ULong): CborInt = CborInt(value, isPositive = true, tags = tags) +/** + * Converts this integer to [Long], throwing if it cannot be represented as [Long]. + */ +@ExperimentalSerializationApi +public val CborInt.long: Long + get() = longOrNull ?: throw ArithmeticException("$this cannot be represented as Long") + +/** + * Converts this integer to [Long], or returns `null` if it cannot be represented as [Long]. + */ +@ExperimentalSerializationApi +public val CborInt.longOrNull: Long? + get() { + val max = Long.MAX_VALUE.toULong() + return if (isPositive) { + if (value <= max) value.toLong() else null + } else { + when { + value <= max -> -value.toLong() + value == max + 1uL -> Long.MIN_VALUE + else -> null + } + } + } + +/** + * Converts this integer to [Int], throwing if it cannot be represented as [Int]. + */ +@ExperimentalSerializationApi +public val CborInt.int: Int + get() = intOrNull ?: throw ArithmeticException("$this cannot be represented as Int") + +/** + * Converts this integer to [Int], or returns `null` if it cannot be represented as [Int]. + */ +@ExperimentalSerializationApi +public val CborInt.intOrNull: Int? + get() { + val longValue = longOrNull ?: return null + if (longValue !in Int.MIN_VALUE.toLong()..Int.MAX_VALUE.toLong()) return null + return longValue.toInt() + } + +/** + * Converts this integer to [Short], throwing if it cannot be represented as [Short]. + */ +@ExperimentalSerializationApi +public val CborInt.short: Short + get() = shortOrNull ?: throw ArithmeticException("$this cannot be represented as Short") + +/** + * Converts this integer to [Short], or returns `null` if it cannot be represented as [Short]. + */ +@ExperimentalSerializationApi +public val CborInt.shortOrNull: Short? + get() { + val longValue = longOrNull ?: return null + if (longValue !in Short.MIN_VALUE.toLong()..Short.MAX_VALUE.toLong()) return null + return longValue.toShort() + } + +/** + * Converts this integer to [Byte], throwing if it cannot be represented as [Byte]. + */ +@ExperimentalSerializationApi +public val CborInt.byte: Byte + get() = byteOrNull ?: throw ArithmeticException("$this cannot be represented as Byte") + +/** + * Converts this integer to [Byte], or returns `null` if it cannot be represented as [Byte]. + */ +@ExperimentalSerializationApi +public val CborInt.byteOrNull: Byte? + get() { + val longValue = longOrNull ?: return null + if (longValue !in Byte.MIN_VALUE.toLong()..Byte.MAX_VALUE.toLong()) return null + return longValue.toByte() + } + /** * Class representing CBOR floating point value (major type 7). */ 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 d22ad070b9..d71d741178 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -680,14 +680,8 @@ internal class StructuredCborParser(internal val element: CborElement, private v if (layer.current !is CborInt) { throw CborDecodingException("Expected number, got ${layer.current::class.simpleName}") } - return (layer.current as CborInt).run { - when (isPositive) { - //@formatter:off - true -> value.toLong() - false -> -value.toLong() //possible loss of precision, but inevitable - //@formatter:on - } - } + return (layer.current as CborInt).longOrNull + ?: throw CborDecodingException("${layer.current} cannot be represented as Long") } override fun nextString(tags: ULongArray?): String { @@ -723,7 +717,11 @@ internal class StructuredCborParser(internal val element: CborElement, private v return when (val key = layer.current) { is CborString -> Triple(key.value, null, tags) - is CborInt -> Triple(null, key.value.toLong(), tags) + is CborInt -> Triple( + null, + key.longOrNull ?: throw CborDecodingException("$key cannot be represented as Long"), + tags + ) else -> throw CborDecodingException("Expected string or number key, got ${key::class.simpleName}") } } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index fb6cb9000a..4b573ed324 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -103,10 +103,9 @@ class CborElementTest { assertEquals(numberElement, decodedNumber) assertEquals(ULong.MAX_VALUE, (decodedNumber as CborInt).value) - val lossOfPrecision = cbor.decodeFromCborElement(numberElement) - - assertEquals(1L, lossOfPrecision) - assertEquals(1L, numberElement.toLong()) + assertNull(numberElement.longOrNull) + assertFailsWith { numberElement.long } + assertFailsWith { cbor.decodeFromCborElement(numberElement) } } @@ -123,15 +122,15 @@ class CborElementTest { val long = cbor.decodeFromCborElement(numberElement) assertEquals(Long.MIN_VALUE+1, long) - assertEquals(Long.MIN_VALUE+1, numberElement.toLong()) + assertEquals(Long.MIN_VALUE + 1, numberElement.long) } @Test fun testCborNumberLong() { - assertEquals(Long.MAX_VALUE, CborInt(Long.MAX_VALUE).toLong()) - assertEquals(Long.MIN_VALUE, CborInt(Long.MIN_VALUE).toLong()) + assertEquals(Long.MAX_VALUE, CborInt(Long.MAX_VALUE).long) + assertEquals(Long.MIN_VALUE, CborInt(Long.MIN_VALUE).long) } @Test From 5dbd9543806b787df7bca24f848642454a178cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 19:27:28 +0100 Subject: [PATCH 48/68] Fix StructuredCborWriter pending tag handling --- .../serialization/cbor/internal/Encoder.kt | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) 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 52c7fc7506..2cb13869a6 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -250,17 +250,15 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { // value tags are collects inside beginStructure, so we need to cache them here and write them in beginStructure or encodeXXX // and then null them out, so there are no leftovers - private var nextValueTags: ULongArray = EMPTY_TAGS - get() { - val ret = field - field = EMPTY_TAGS - return ret - } + private var pendingValueTags: ULongArray = EMPTY_TAGS + + private fun takePendingValueTags(): ULongArray = + pendingValueTags.also { pendingValueTags = EMPTY_TAGS } fun finalize() = currentElement!!.finalize() override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { - val tags = nextValueTags + + val tags = takePendingValueTags() + if (cbor.configuration.encodeObjectTags) descriptor.getObjectTags() ?: EMPTY_TAGS else EMPTY_TAGS val element = if (descriptor.hasArrayTag()) { @@ -315,7 +313,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { if (cbor.configuration.encodeValueTags) { descriptor.getValueTags(index).let { valueTags -> //collect them for late encoding in beginStructure or encodeXXX - nextValueTags = valueTags ?: EMPTY_TAGS + pendingValueTags = valueTags ?: EMPTY_TAGS } } return true @@ -323,72 +321,74 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { override fun encodeTags(tags: ULongArray) { - nextValueTags += tags + if (tags.isEmpty()) return + pendingValueTags = if (pendingValueTags.isEmpty()) tags else pendingValueTags + tags } override fun encodeBoolean(value: Boolean) { - currentElement += CborBoolean(value, tags = nextValueTags) + currentElement += CborBoolean(value, tags = takePendingValueTags()) } override fun encodeByte(value: Byte) { - currentElement += CborInt(value.toLong(), tags = nextValueTags) + currentElement += CborInt(value.toLong(), tags = takePendingValueTags()) } override fun encodeChar(value: Char) { - currentElement += CborInt(value.code.toLong(), tags = nextValueTags) + currentElement += CborInt(value.code.toLong(), tags = takePendingValueTags()) } override fun encodeDouble(value: Double) { - currentElement += CborFloat(value, tags = nextValueTags) + currentElement += CborFloat(value, tags = takePendingValueTags()) } override fun encodeFloat(value: Float) { - currentElement += CborFloat(value.toDouble(), tags = nextValueTags) + currentElement += CborFloat(value.toDouble(), tags = takePendingValueTags()) } override fun encodeInt(value: Int) { - currentElement += CborInt(value.toLong(), tags = nextValueTags) + currentElement += CborInt(value.toLong(), tags = takePendingValueTags()) } override fun encodeLong(value: Long) { - currentElement += CborInt(value, tags = nextValueTags) + currentElement += CborInt(value, tags = takePendingValueTags()) } override fun encodeNegative(value: ULong) { - currentElement += CborInt(value, isPositive = false, tags = nextValueTags) + currentElement += CborInt(value, isPositive = false, tags = takePendingValueTags()) } override fun encodePositive(value: ULong) { - currentElement += CborInt(value, isPositive = true, tags = nextValueTags) + currentElement += CborInt(value, isPositive = true, tags = takePendingValueTags()) } override fun encodeShort(value: Short) { - currentElement += CborInt(value.toLong(), tags = nextValueTags) + currentElement += CborInt(value.toLong(), tags = takePendingValueTags()) } override fun encodeString(value: String) { - currentElement += CborString(value, tags = nextValueTags) + currentElement += CborString(value, tags = takePendingValueTags()) } override fun encodeByteString(byteArray: ByteArray) { - currentElement += CborByteString(byteArray, tags = nextValueTags) + currentElement += CborByteString(byteArray, tags = takePendingValueTags()) } override fun encodeUndefined() { - currentElement += CborUndefined(tags = nextValueTags) + currentElement += CborUndefined(tags = takePendingValueTags()) } override fun encodeNull() { + val tags = takePendingValueTags() currentElement += if (encodeNullAsEmptyMap) CborMap( mapOf(), - tags = nextValueTags + tags = tags ) - else CborNull(tags = nextValueTags) + else CborNull(tags = tags) } override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) { - currentElement += CborString(enumDescriptor.getElementName(index), tags = nextValueTags) + currentElement += CborString(enumDescriptor.getElementName(index), tags = takePendingValueTags()) } } From 23c40253b88ec7dfddc644e0ee15831b043648c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 19:40:10 +0100 Subject: [PATCH 49/68] Fix CBOR container --- .../serialization/cbor/internal/Encoder.kt | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) 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 2cb13869a6..83c5e12a61 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -197,12 +197,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { * Tags and values are "written", i.e. recorded/prepared for encoding separately. Hence, we need a helper that allows * for setting tags and values independently, and then merging them into the final [CborElement] at the end. */ - internal sealed class CborContainer(tags: ULongArray, elements: MutableList) { - protected val elements = elements - - var tags = tags - internal set - + internal sealed class CborContainer(private val tags: ULongArray, protected val elements: MutableList) { open fun add(element: CborElement) = elements.add(element) class Map(tags: ULongArray, elements: MutableList = mutableListOf()) : @@ -211,7 +206,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { class List(tags: ULongArray, elements: MutableList = mutableListOf()) : CborContainer(tags, elements) - class Primitive(tags: ULongArray) : CborContainer(tags, elements = mutableListOf()) { + class Root(tags: ULongArray) : CborContainer(tags, elements = mutableListOf()) { override fun add(element: CborElement): Boolean { require(elements.isEmpty()) { "Implementation error. Please report a bug." } return elements.add(element) @@ -236,7 +231,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { tags = tags ) - is Primitive -> elements.first().also { it.tags += tags } + is Root -> elements.first().also { it.tags += tags } } } @@ -246,7 +241,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { } private val stack = ArrayDeque() - private var currentElement: CborContainer? = CborContainer.Primitive(tags = EMPTY_TAGS) + private var currentElement: CborContainer = CborContainer.Root(tags = EMPTY_TAGS) // value tags are collects inside beginStructure, so we need to cache them here and write them in beginStructure or encodeXXX // and then null them out, so there are no leftovers @@ -255,7 +250,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { private fun takePendingValueTags(): ULongArray = pendingValueTags.also { pendingValueTags = EMPTY_TAGS } - fun finalize() = currentElement!!.finalize() + fun finalize() = currentElement.finalize() override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { val tags = takePendingValueTags() + From 57f37f31165d0291d080a736e59b13301827311a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 22:35:25 +0100 Subject: [PATCH 50/68] Test structured CBOR root primitives and lists --- .../kotlinx/serialization/cbor/CborElementTest.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index 4b573ed324..815724a66d 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -15,7 +15,9 @@ class CborElementTest { @OptIn(ExperimentalSerializationApi::class) @Test fun testEncodeToCborElementRootPrimitiveInt() { - assertEquals(CborInt(42), cbor.encodeToCborElement(42)) + val element = cbor.encodeToCborElement(42) + assertEquals(CborInt(42), element) + assertEquals(42, cbor.decodeFromCborElement(element)) } @OptIn(ExperimentalSerializationApi::class) @@ -25,6 +27,7 @@ class CborElementTest { val element = configured.encodeToCborElement(byteArrayOf(1, 2, 3)) assertTrue(element is CborByteString) assertTrue(element.value.contentEquals(byteArrayOf(1, 2, 3))) + assertTrue(configured.decodeFromCborElement(element).contentEquals(byteArrayOf(1, 2, 3))) } @Serializable @@ -41,6 +44,15 @@ class CborElementTest { assertEquals(wrapper, cbor.decodeFromCborElement(element)) } + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testEncodeDecodeRootListViaCborElement() { + val value = listOf(1, 2, 3) + val element = cbor.encodeToCborElement(value) + assertTrue(element is CborList) + assertEquals(value, cbor.decodeFromCborElement>(element)) + } + @Test fun testCborNull() { val nullElement = CborNull() From 9f096834b520e65da884c6d1f8b4957c14ab48fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 22:46:22 +0100 Subject: [PATCH 51/68] Fix typed CborElement decoding and add tests --- .../serialization/cbor/internal/Decoder.kt | 9 +- .../cbor/CborElementSerializerBehaviorTest.kt | 104 ++++++++++++++++++ 2 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementSerializerBehaviorTest.kt 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 d71d741178..98be06bbb3 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -116,12 +116,11 @@ internal open class CborReader(override val cbor: Cbor, protected val parser: Cb @OptIn(ExperimentalSerializationApi::class) override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { - @Suppress("UNCHECKED_CAST") return if (deserializer is CborSerializer) { - val tags = parser.processTags(tags) - decodeCborElement().also { /*this is a NOOP for structured parser but not from bytes */it.tags = - tags ?: EMPTY_TAGS - } as T + val collectedTags = parser.processTags(tags) ?: EMPTY_TAGS + deserializer.deserialize(this).also { value -> + (value as? CborElement)?.tags = collectedTags + } } else if ((decodeByteArrayAsByteString || cbor.configuration.alwaysUseByteString) && deserializer.descriptor == ByteArraySerializer().descriptor ) { diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementSerializerBehaviorTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementSerializerBehaviorTest.kt new file mode 100644 index 0000000000..5c0845537a --- /dev/null +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementSerializerBehaviorTest.kt @@ -0,0 +1,104 @@ +package kotlinx.serialization.cbor + +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.cbor.internal.CborDecodingException +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.encoding.CompositeEncoder +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.modules.SerializersModule +import kotlin.test.Test +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class CborElementSerializerBehaviorTest { + + private object NonCborEncoder : Encoder { + override val serializersModule: SerializersModule = EmptySerializersModule() + override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder = error("unused") + override fun encodeBoolean(value: Boolean) = error("unused") + override fun encodeByte(value: Byte) = error("unused") + override fun encodeChar(value: Char) = error("unused") + override fun encodeDouble(value: Double) = error("unused") + override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) = error("unused") + override fun encodeFloat(value: Float) = error("unused") + override fun encodeInline(descriptor: SerialDescriptor): Encoder = error("unused") + override fun encodeInt(value: Int) = error("unused") + override fun encodeLong(value: Long) = error("unused") + override fun encodeNotNullMark() = error("unused") + override fun encodeNull() = error("unused") + override fun encodeShort(value: Short) = error("unused") + override fun encodeString(value: String) = error("unused") + override fun encodeSerializableValue(serializer: SerializationStrategy, value: T) = error("unused") + } + + private object NonCborDecoder : Decoder { + override val serializersModule: SerializersModule = EmptySerializersModule() + override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder = error("unused") + override fun decodeBoolean(): Boolean = error("unused") + override fun decodeByte(): Byte = error("unused") + override fun decodeChar(): Char = error("unused") + override fun decodeDouble(): Double = error("unused") + override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = error("unused") + override fun decodeFloat(): Float = error("unused") + override fun decodeInline(descriptor: SerialDescriptor): Decoder = error("unused") + override fun decodeInt(): Int = error("unused") + override fun decodeLong(): Long = error("unused") + override fun decodeNotNullMark(): Boolean = error("unused") + override fun decodeNull(): Nothing? = error("unused") + override fun decodeShort(): Short = error("unused") + override fun decodeString(): String = error("unused") + override fun decodeSerializableValue(deserializer: DeserializationStrategy): T = error("unused") + } + + @Test + fun cborElementSerializerRequiresCborEncoder() { + val ex = assertFailsWith { + CborElement.serializer().serialize(NonCborEncoder, CborInt(1)) + } + assertTrue(ex.message?.contains("This serializer can be used only with Cbor format") == true) + } + + @Test + fun cborElementSerializerRequiresCborDecoder() { + val ex = assertFailsWith { + CborElement.serializer().deserialize(NonCborDecoder) + } + assertTrue(ex.message?.contains("This serializer can be used only with Cbor format") == true) + } + + @Test + fun typedCborListDeserializationFailsOnNonListInput() { + val cbor = Cbor {} + val bytes = cbor.encodeToByteArray(CborElement.serializer(), CborInt(1)) + assertFailsWith { + cbor.decodeFromByteArray(CborList.serializer(), bytes) + } + assertFailsWith { + cbor.decodeFromCborElement(CborList.serializer(), CborInt(1)) + } + } + + @Test + fun typedCborIntDeserializationFailsOnNonIntInput() { + val cbor = Cbor {} + val bytes = cbor.encodeToByteArray(CborElement.serializer(), CborList(listOf(CborInt(1)))) + assertFailsWith { + cbor.decodeFromByteArray(CborInt.serializer(), bytes) + } + assertFailsWith { + cbor.decodeFromCborElement(CborInt.serializer(), CborList(listOf(CborInt(1)))) + } + } + + @Test + fun structuredByteStringDecodesToByteArray() { + val cbor = Cbor { alwaysUseByteString = true } + val element = cbor.encodeToCborElement(byteArrayOf(1, 2, 3)) + assertTrue(element is CborByteString) + assertTrue(cbor.decodeFromCborElement(element).contentEquals(byteArrayOf(1, 2, 3))) + } +} From c3714e11ae7395d0d218475e9154b5699aff8ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 23:08:39 +0100 Subject: [PATCH 52/68] Validate encodeTags is followed by a data item --- .../src/kotlinx/serialization/cbor/Cbor.kt | 2 + .../serialization/cbor/internal/Encoder.kt | 76 +++++++++++++++++-- .../CborEncoderEncodeTagsValidationTest.kt | 74 ++++++++++++++++++ 3 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborEncoderEncodeTagsValidationTest.kt diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt index 01b0d22381..e2aa5135b8 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt @@ -78,6 +78,7 @@ public sealed class Cbor( output ) dumper.encodeSerializableValue(serializer, value) + dumper.ensureNoDanglingTagsAtEndOfSerialization() return output.toByteArray() @@ -114,6 +115,7 @@ public sealed class Cbor( public fun encodeToCborElement(serializer: SerializationStrategy, value: T): CborElement { val writer = StructuredCborWriter(this) writer.encodeSerializableValue(serializer, value) + writer.ensureNoDanglingTagsAtEndOfSerialization() return writer.finalize() } } 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 83c5e12a61..bedcf85880 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -28,11 +28,38 @@ internal sealed class CborWriter( override val cbor: Cbor, ) : AbstractEncoder(), CborEncoder { + private var tagsMustBeFollowedByDataItem: Boolean = false + + protected fun onDataItemEncoded() { + tagsMustBeFollowedByDataItem = false + } + + protected fun ensureNoDanglingTags(context: String) { + if (tagsMustBeFollowedByDataItem) { + throw SerializationException( + "Invalid CBOR encoding: encodeTags() must be followed by another encode* call that writes a CBOR data item ($context)." + ) + } + } + + internal fun ensureNoDanglingTagsAtEndOfSerialization() { + ensureNoDanglingTags("end of serialization") + } + + final override fun encodeTags(tags: ULongArray) { + if (tags.isNotEmpty()) tagsMustBeFollowedByDataItem = true + encodeTagsImpl(tags) + } + + protected abstract fun encodeTagsImpl(tags: ULongArray) + override fun encodeByteString(byteArray: ByteArray) { + onDataItemEncoded() getDestination().encodeByteString(byteArray) } override fun encodeUndefined() { + onDataItemEncoded() getDestination().encodeUndefined() } @@ -67,51 +94,68 @@ internal sealed class CborWriter( protected abstract fun incrementChildren() override fun encodeString(value: String) { + onDataItemEncoded() getDestination().encodeString(value) } override fun encodeFloat(value: Float) { + onDataItemEncoded() getDestination().encodeFloat(value) } override fun encodeDouble(value: Double) { + onDataItemEncoded() getDestination().encodeDouble(value) } override fun encodeChar(value: Char) { + onDataItemEncoded() getDestination().encodeNumber(value.code.toLong()) } override fun encodeByte(value: Byte) { + onDataItemEncoded() getDestination().encodeNumber(value.toLong()) } override fun encodeShort(value: Short) { + onDataItemEncoded() getDestination().encodeNumber(value.toLong()) } override fun encodeInt(value: Int) { + onDataItemEncoded() getDestination().encodeNumber(value.toLong()) } override fun encodeLong(value: Long) { + onDataItemEncoded() getDestination().encodeNumber(value) } - override fun encodeNegative(value: ULong) = getDestination().encodeNegative(value) - override fun encodePositive(value: ULong) = getDestination().encodePositive(value) + override fun encodeNegative(value: ULong) { + onDataItemEncoded() + getDestination().encodeNegative(value) + } + + override fun encodePositive(value: ULong) { + onDataItemEncoded() + getDestination().encodePositive(value) + } override fun encodeBoolean(value: Boolean) { + onDataItemEncoded() getDestination().encodeBoolean(value) } override fun encodeNull() { + onDataItemEncoded() if (encodeNullAsEmptyMap) getDestination().encodeEmptyMap() else getDestination().encodeNull() } @@ -121,6 +165,7 @@ internal sealed class CborWriter( enumDescriptor: SerialDescriptor, index: Int ) { + onDataItemEncoded() getDestination().encodeString(enumDescriptor.getElementName(index)) } @@ -161,6 +206,7 @@ internal class IndefiniteLengthCborWriter(cbor: Cbor, private val output: ByteAr ) { override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { + onDataItemEncoded() if (cbor.configuration.encodeObjectTags) descriptor.getObjectTags()?.forEach { output.encodeTag(it) } @@ -177,6 +223,7 @@ internal class IndefiniteLengthCborWriter(cbor: Cbor, private val output: ByteAr } override fun endStructure(descriptor: SerialDescriptor) { + ensureNoDanglingTags("endStructure") output.end() } @@ -186,7 +233,7 @@ internal class IndefiniteLengthCborWriter(cbor: Cbor, private val output: ByteAr override fun incrementChildren() {/*NOOP*/ } - override fun encodeTags(tags: ULongArray) = tags.forEach { getDestination().encodeTag(it) } + override fun encodeTagsImpl(tags: ULongArray) = tags.forEach { getDestination().encodeTag(it) } } @@ -253,6 +300,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { fun finalize() = currentElement.finalize() override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { + onDataItemEncoded() val tags = takePendingValueTags() + if (cbor.configuration.encodeObjectTags) descriptor.getObjectTags() ?: EMPTY_TAGS else EMPTY_TAGS @@ -271,6 +319,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { } override fun endStructure(descriptor: SerialDescriptor) { + ensureNoDanglingTags("endStructure") val finalized = currentElement!!.finalize() if (stack.isNotEmpty()) { currentElement = stack.removeLast() @@ -315,65 +364,79 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { } - override fun encodeTags(tags: ULongArray) { + override fun encodeTagsImpl(tags: ULongArray) { if (tags.isEmpty()) return pendingValueTags = if (pendingValueTags.isEmpty()) tags else pendingValueTags + tags } override fun encodeBoolean(value: Boolean) { + onDataItemEncoded() currentElement += CborBoolean(value, tags = takePendingValueTags()) } override fun encodeByte(value: Byte) { + onDataItemEncoded() currentElement += CborInt(value.toLong(), tags = takePendingValueTags()) } override fun encodeChar(value: Char) { + onDataItemEncoded() currentElement += CborInt(value.code.toLong(), tags = takePendingValueTags()) } override fun encodeDouble(value: Double) { + onDataItemEncoded() currentElement += CborFloat(value, tags = takePendingValueTags()) } override fun encodeFloat(value: Float) { + onDataItemEncoded() currentElement += CborFloat(value.toDouble(), tags = takePendingValueTags()) } override fun encodeInt(value: Int) { + onDataItemEncoded() currentElement += CborInt(value.toLong(), tags = takePendingValueTags()) } override fun encodeLong(value: Long) { + onDataItemEncoded() currentElement += CborInt(value, tags = takePendingValueTags()) } override fun encodeNegative(value: ULong) { + onDataItemEncoded() currentElement += CborInt(value, isPositive = false, tags = takePendingValueTags()) } override fun encodePositive(value: ULong) { + onDataItemEncoded() currentElement += CborInt(value, isPositive = true, tags = takePendingValueTags()) } override fun encodeShort(value: Short) { + onDataItemEncoded() currentElement += CborInt(value.toLong(), tags = takePendingValueTags()) } override fun encodeString(value: String) { + onDataItemEncoded() currentElement += CborString(value, tags = takePendingValueTags()) } override fun encodeByteString(byteArray: ByteArray) { + onDataItemEncoded() currentElement += CborByteString(byteArray, tags = takePendingValueTags()) } override fun encodeUndefined() { + onDataItemEncoded() currentElement += CborUndefined(tags = takePendingValueTags()) } override fun encodeNull() { + onDataItemEncoded() val tags = takePendingValueTags() currentElement += if (encodeNullAsEmptyMap) CborMap( mapOf(), @@ -383,6 +446,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { } override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) { + onDataItemEncoded() currentElement += CborString(enumDescriptor.getElementName(index), tags = takePendingValueTags()) } @@ -400,15 +464,17 @@ internal class DefiniteLengthCborWriter(cbor: Cbor, output: ByteArrayOutput) : C structureStack.peek().elementCount++ } - override fun encodeTags(tags: ULongArray) = tags.forEach { getDestination().encodeTag(it) } + override fun encodeTagsImpl(tags: ULongArray) = tags.forEach { getDestination().encodeTag(it) } override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { + onDataItemEncoded() val current = Data(ByteArrayOutput(), 0) val _ = structureStack.push(current) return this } override fun endStructure(descriptor: SerialDescriptor) { + ensureNoDanglingTags("endStructure") val completedCurrent = structureStack.pop() val accumulator = getDestination() diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborEncoderEncodeTagsValidationTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborEncoderEncodeTagsValidationTest.kt new file mode 100644 index 0000000000..efea2709af --- /dev/null +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborEncoderEncodeTagsValidationTest.kt @@ -0,0 +1,74 @@ +package kotlinx.serialization.cbor + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.builtins.* +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.CompositeEncoder +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlin.test.Test +import kotlin.test.assertFailsWith + +@OptIn(ExperimentalSerializationApi::class, ExperimentalUnsignedTypes::class) +class CborEncoderEncodeTagsValidationTest { + + private object DanglingRootTagsSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("DanglingRootTags") + + override fun serialize(encoder: Encoder, value: Unit) { + (encoder as CborEncoder).encodeTags(ulongArrayOf(1u)) + } + + override fun deserialize(decoder: Decoder): Unit = Unit + } + + private object DanglingTagsInStructureSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("DanglingTagsInStructure") { + element("x", Int.serializer().descriptor) + } + + override fun serialize(encoder: Encoder, value: Unit) { + val composite = encoder.beginStructure(descriptor) + (composite as CborEncoder).encodeTags(ulongArrayOf(1u)) + composite.endStructure(descriptor) + } + + override fun deserialize(decoder: Decoder): Unit = Unit + } + + @Test + fun danglingEncodeTagsFailsForByteEncoding_indefinite() { + val cbor = Cbor { useDefiniteLengthEncoding = false } + assertFailsWith { + cbor.encodeToByteArray(DanglingRootTagsSerializer, Unit) + } + assertFailsWith { + cbor.encodeToByteArray(DanglingTagsInStructureSerializer, Unit) + } + } + + @Test + fun danglingEncodeTagsFailsForByteEncoding_definite() { + val cbor = Cbor { useDefiniteLengthEncoding = true } + assertFailsWith { + cbor.encodeToByteArray(DanglingRootTagsSerializer, Unit) + } + assertFailsWith { + cbor.encodeToByteArray(DanglingTagsInStructureSerializer, Unit) + } + } + + @Test + fun danglingEncodeTagsFailsForStructuredEncoding() { + val cbor = Cbor {} + assertFailsWith { + cbor.encodeToCborElement(DanglingRootTagsSerializer, Unit) + } + assertFailsWith { + cbor.encodeToCborElement(DanglingTagsInStructureSerializer, Unit) + } + } +} From e02edb2d147f4b3a8dad70a90ae387cab4906218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 23:14:40 +0100 Subject: [PATCH 53/68] CborInt -> CborInteger --- .../kotlinx/serialization/cbor/CborElement.kt | 30 ++++++------ .../kotlinx/serialization/cbor/CborEncoder.kt | 2 +- .../cbor/internal/CborElementSerializers.kt | 10 ++-- .../cbor/internal/CborTreeReader.kt | 4 +- .../serialization/cbor/internal/Decoder.kt | 6 +-- .../serialization/cbor/internal/Encoder.kt | 4 +- .../cbor/CborElementSerializerBehaviorTest.kt | 4 +- .../serialization/cbor/CborElementTest.kt | 46 +++++++++---------- 8 files changed, 53 insertions(+), 53 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt index 73e878048b..5c245e6db5 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -103,7 +103,7 @@ public sealed class CborPrimitive( */ @Serializable(with = CborIntSerializer::class) @ExperimentalSerializationApi -public class CborInt( +public class CborInteger( absoluteValue: ULong, public val isPositive: Boolean, vararg tags: ULong @@ -116,7 +116,7 @@ public class CborInt( override fun equals(other: Any?): Boolean { if (this === other) return true - if (other !is CborInt) return false + if (other !is CborInteger) return false if (!super.equals(other)) return false if (isPositive != other.isPositive) return false @@ -151,30 +151,30 @@ public class CborInt( */ @ExperimentalSerializationApi @Suppress("FunctionName") -public fun CborInt(value: Long, vararg tags: ULong): CborInt = - if (value >= 0L) CborInt(value.toULong(), isPositive = true, tags = tags) - else CborInt(ULong.MAX_VALUE - value.toULong() + 1uL, isPositive = false, tags = tags) +public fun CborInt(value: Long, vararg tags: ULong): CborInteger = + if (value >= 0L) CborInteger(value.toULong(), isPositive = true, tags = tags) + else CborInteger(ULong.MAX_VALUE - value.toULong() + 1uL, isPositive = false, tags = tags) /** * Creates an unsigned CBOR integer (major type 0). */ @ExperimentalSerializationApi @Suppress("FunctionName") -public fun CborInt(value: ULong, vararg tags: ULong): CborInt = - CborInt(value, isPositive = true, tags = tags) +public fun CborInt(value: ULong, vararg tags: ULong): CborInteger = + CborInteger(value, isPositive = true, tags = tags) /** * Converts this integer to [Long], throwing if it cannot be represented as [Long]. */ @ExperimentalSerializationApi -public val CborInt.long: Long +public val CborInteger.long: Long get() = longOrNull ?: throw ArithmeticException("$this cannot be represented as Long") /** * Converts this integer to [Long], or returns `null` if it cannot be represented as [Long]. */ @ExperimentalSerializationApi -public val CborInt.longOrNull: Long? +public val CborInteger.longOrNull: Long? get() { val max = Long.MAX_VALUE.toULong() return if (isPositive) { @@ -192,14 +192,14 @@ public val CborInt.longOrNull: Long? * Converts this integer to [Int], throwing if it cannot be represented as [Int]. */ @ExperimentalSerializationApi -public val CborInt.int: Int +public val CborInteger.int: Int get() = intOrNull ?: throw ArithmeticException("$this cannot be represented as Int") /** * Converts this integer to [Int], or returns `null` if it cannot be represented as [Int]. */ @ExperimentalSerializationApi -public val CborInt.intOrNull: Int? +public val CborInteger.intOrNull: Int? get() { val longValue = longOrNull ?: return null if (longValue !in Int.MIN_VALUE.toLong()..Int.MAX_VALUE.toLong()) return null @@ -210,14 +210,14 @@ public val CborInt.intOrNull: Int? * Converts this integer to [Short], throwing if it cannot be represented as [Short]. */ @ExperimentalSerializationApi -public val CborInt.short: Short +public val CborInteger.short: Short get() = shortOrNull ?: throw ArithmeticException("$this cannot be represented as Short") /** * Converts this integer to [Short], or returns `null` if it cannot be represented as [Short]. */ @ExperimentalSerializationApi -public val CborInt.shortOrNull: Short? +public val CborInteger.shortOrNull: Short? get() { val longValue = longOrNull ?: return null if (longValue !in Short.MIN_VALUE.toLong()..Short.MAX_VALUE.toLong()) return null @@ -228,14 +228,14 @@ public val CborInt.shortOrNull: Short? * Converts this integer to [Byte], throwing if it cannot be represented as [Byte]. */ @ExperimentalSerializationApi -public val CborInt.byte: Byte +public val CborInteger.byte: Byte get() = byteOrNull ?: throw ArithmeticException("$this cannot be represented as Byte") /** * Converts this integer to [Byte], or returns `null` if it cannot be represented as [Byte]. */ @ExperimentalSerializationApi -public val CborInt.byteOrNull: Byte? +public val CborInteger.byteOrNull: Byte? get() { val longValue = longOrNull ?: return null if (longValue !in Byte.MIN_VALUE.toLong()..Byte.MAX_VALUE.toLong()) return null diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt index 9c68c8966a..60c1f09db7 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt @@ -82,7 +82,7 @@ public interface CborEncoder : Encoder { /** * Encode a negative integer (major type 1) with an absolute value that may exceed [Long.MIN_VALUE]. * - * The encoded CBOR value is `-1 - (value - 1)` (i.e. `-value` in terms of [CborInt]'s absolute representation). + * The encoded CBOR value is `-1 - (value - 1)` (i.e. `-value` in terms of [CborInteger]'s absolute representation). */ public fun encodeNegative(value: ULong) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index d51c7ef54a..6aa5197c4d 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -66,7 +66,7 @@ internal object CborPrimitiveSerializer : KSerializer, CborSerial is CborBoolean -> encoder.encodeSerializableValue(CborBooleanSerializer, value) is CborByteString -> encoder.encodeSerializableValue(CborByteStringSerializer, value) is CborFloat -> encoder.encodeSerializableValue(CborFloatSerializer, value) - is CborInt -> encoder.encodeSerializableValue(CborIntSerializer, value) + is CborInteger -> encoder.encodeSerializableValue(CborIntSerializer, value) } } @@ -117,11 +117,11 @@ internal object CborUndefinedSerializer : KSerializer, CborSerial } -internal object CborIntSerializer : KSerializer, CborSerializer { +internal object CborIntSerializer : KSerializer, CborSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborInt", PrimitiveKind.LONG) - override fun serialize(encoder: Encoder, value: CborInt) { + override fun serialize(encoder: Encoder, value: CborInteger) { val cborEncoder = encoder.asCborEncoder() cborEncoder.encodeTags(value.tags) when (value.isPositive) { @@ -132,9 +132,9 @@ internal object CborIntSerializer : KSerializer, CborSerializer { } } - override fun deserialize(decoder: Decoder): CborInt { + override fun deserialize(decoder: Decoder): CborInteger { val result = decoder.asCborDecoder().decodeCborElement() - if (result !is CborInt) throw CborDecodingException("Unexpected CBOR element, expected CborInt, had ${result::class}") + if (result !is CborInteger) throw CborDecodingException("Unexpected CBOR element, expected CborInt, had ${result::class}") return result } } diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt index 5e64d3c74a..7f22ef8b2e 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt @@ -28,12 +28,12 @@ internal class CborTreeReader( val result = when (parser.curByte shr 5) { // Get major type from the first 3 bits 0 -> { // Major type 0: unsigned integer val value = parser.nextULong() - CborInt(value, isPositive = true, tags = tags) + CborInteger(value, isPositive = true, tags = tags) } 1 -> { // Major type 1: negative integer val value = parser.nextULong() + 1uL - CborInt(value, isPositive = false, tags = tags) + CborInteger(value, isPositive = false, tags = tags) } 2 -> { // Major type 2: byte string 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 98be06bbb3..9a59124c67 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -676,10 +676,10 @@ internal class StructuredCborParser(internal val element: CborElement, private v override fun nextNumber(tags: ULongArray?): Long { processTags(tags) - if (layer.current !is CborInt) { + if (layer.current !is CborInteger) { throw CborDecodingException("Expected number, got ${layer.current::class.simpleName}") } - return (layer.current as CborInt).longOrNull + return (layer.current as CborInteger).longOrNull ?: throw CborDecodingException("${layer.current} cannot be represented as Long") } @@ -716,7 +716,7 @@ internal class StructuredCborParser(internal val element: CborElement, private v return when (val key = layer.current) { is CborString -> Triple(key.value, null, tags) - is CborInt -> Triple( + is CborInteger -> Triple( null, key.longOrNull ?: throw CborDecodingException("$key cannot be represented as Long"), tags 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 bedcf85880..12c116bfbd 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -406,12 +406,12 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { override fun encodeNegative(value: ULong) { onDataItemEncoded() - currentElement += CborInt(value, isPositive = false, tags = takePendingValueTags()) + currentElement += CborInteger(value, isPositive = false, tags = takePendingValueTags()) } override fun encodePositive(value: ULong) { onDataItemEncoded() - currentElement += CborInt(value, isPositive = true, tags = takePendingValueTags()) + currentElement += CborInteger(value, isPositive = true, tags = takePendingValueTags()) } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementSerializerBehaviorTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementSerializerBehaviorTest.kt index 5c0845537a..7bc66524cf 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementSerializerBehaviorTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementSerializerBehaviorTest.kt @@ -87,10 +87,10 @@ class CborElementSerializerBehaviorTest { val cbor = Cbor {} val bytes = cbor.encodeToByteArray(CborElement.serializer(), CborList(listOf(CborInt(1)))) assertFailsWith { - cbor.decodeFromByteArray(CborInt.serializer(), bytes) + cbor.decodeFromByteArray(CborInteger.serializer(), bytes) } assertFailsWith { - cbor.decodeFromCborElement(CborInt.serializer(), CborList(listOf(CborInt(1)))) + cbor.decodeFromCborElement(CborInteger.serializer(), CborList(listOf(CborInt(1)))) } } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index 815724a66d..56d8cd2e91 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -67,7 +67,7 @@ class CborElementTest { val numberBytes = cbor.encodeToByteArray(numberElement) val decodedNumber = cbor.decodeFromByteArray(numberBytes) assertEquals(numberElement, decodedNumber) - assertEquals(42u, (decodedNumber as CborInt).value) + assertEquals(42u, (decodedNumber as CborInteger).value) } @Test @@ -79,7 +79,7 @@ class CborElementTest { val numberBytes = cbor.encodeToByteArray(numberElement) val decodedNumber = cbor.decodeFromByteArray(numberBytes) assertEquals(numberElement, decodedNumber) - assertEquals(0uL, (decodedNumber as CborInt).value) + assertEquals(0uL, (decodedNumber as CborInteger).value) } @Test @@ -90,7 +90,7 @@ class CborElementTest { val numberBytes = cbor.encodeToByteArray(numberElement) val decodedNumber = cbor.decodeFromByteArray(numberBytes) assertEquals(numberElement, decodedNumber) - assertEquals(ULong.MAX_VALUE, (decodedNumber as CborInt).value) + assertEquals(ULong.MAX_VALUE, (decodedNumber as CborInteger).value) } @Test @@ -101,19 +101,19 @@ class CborElementTest { val numberBytes = cbor.encodeToByteArray(numberElement) val decodedNumber = cbor.decodeFromByteArray(numberBytes) assertEquals(numberElement, decodedNumber) - assertEquals(Long.MAX_VALUE.toULong(), (decodedNumber as CborInt).value) + assertEquals(Long.MAX_VALUE.toULong(), (decodedNumber as CborInteger).value) } @Test fun testCborNumberMin() { - val numberElement = CborInt(ULong.MAX_VALUE, isPositive = false) + val numberElement = CborInteger(ULong.MAX_VALUE, isPositive = false) assertEquals(numberElement.isPositive, false) assertEquals(numberElement.value, ULong.MAX_VALUE) val numberBytes = cbor.encodeToByteArray(numberElement) val decodedNumber = cbor.decodeFromByteArray(numberBytes) assertEquals(numberElement, decodedNumber) - assertEquals(ULong.MAX_VALUE, (decodedNumber as CborInt).value) + assertEquals(ULong.MAX_VALUE, (decodedNumber as CborInteger).value) assertNull(numberElement.longOrNull) assertFailsWith { numberElement.long } @@ -123,13 +123,13 @@ class CborElementTest { @Test fun testCborNumberMinHalv() { - val numberElement = CborInt(Long.MAX_VALUE.toULong(), isPositive = false) + val numberElement = CborInteger(Long.MAX_VALUE.toULong(), isPositive = false) assertEquals(numberElement.isPositive, false) assertEquals(numberElement.value, Long.MAX_VALUE.toULong()) val numberBytes = cbor.encodeToByteArray(numberElement) val decodedNumber = cbor.decodeFromByteArray(numberBytes) assertEquals(numberElement, decodedNumber) - assertEquals(Long.MAX_VALUE.toULong(), (decodedNumber as CborInt).value) + assertEquals(Long.MAX_VALUE.toULong(), (decodedNumber as CborInteger).value) val long = cbor.decodeFromCborElement(numberElement) @@ -191,8 +191,8 @@ class CborElementTest { assertEquals(4, decodedList.size) // Verify individual elements - assertTrue(decodedList[0] is CborInt) - assertEquals(1u, (decodedList[0] as CborInt).value) + assertTrue(decodedList[0] is CborInteger) + assertEquals(1u, (decodedList[0] as CborInteger).value) assertTrue(decodedList[1] is CborString) assertEquals("two", (decodedList[1] as CborString).value) @@ -228,8 +228,8 @@ class CborElementTest { // Verify individual entries assertTrue(decodedMap.containsKey(CborString("key1"))) val value1 = decodedMap[CborString("key1")] - assertTrue(value1 is CborInt) - assertEquals(42u, (value1 as CborInt).value) + assertTrue(value1 is CborInteger) + assertEquals(42u, (value1 as CborInteger).value) assertTrue(decodedMap.containsKey(CborString("key2"))) val value2 = decodedMap[CborString("key2")] @@ -287,8 +287,8 @@ class CborElementTest { assertEquals(5, primitivesValue.size) - assertTrue(primitivesValue[0] is CborInt) - assertEquals(123u, (primitivesValue[0] as CborInt).value) + assertTrue(primitivesValue[0] is CborInteger) + assertEquals(123u, (primitivesValue[0] as CborInteger).value) assertTrue(primitivesValue[1] is CborString) assertEquals("text", (primitivesValue[1] as CborString).value) @@ -315,11 +315,11 @@ class CborElementTest { assertEquals(2, innerValue.size) - assertTrue(innerValue[0] is CborInt) - assertEquals(1u, (innerValue[0] as CborInt).value) + assertTrue(innerValue[0] is CborInteger) + assertEquals(1u, (innerValue[0] as CborInteger).value) - assertTrue(innerValue[1] is CborInt) - assertEquals(2u, (innerValue[1] as CborInt).value) + assertTrue(innerValue[1] is CborInteger) + assertEquals(2u, (innerValue[1] as CborInteger).value) // Verify the empty list assertTrue(nestedValue.containsKey(CborString("empty"))) @@ -333,7 +333,7 @@ class CborElementTest { @Test fun testDecodePositiveInt() { // Test data from CborParserTest.testParseIntegers - val element = cbor.decodeFromHexString("0C") as CborInt + val element = cbor.decodeFromHexString("0C") as CborInteger assertEquals(12u, element.value) } @@ -379,9 +379,9 @@ class CborElementTest { assertTrue(element is CborList) val list = element as CborList assertEquals(3, list.size) - assertEquals(1u, (list[0] as CborInt).value) - assertEquals(255u, (list[1] as CborInt).value) - assertEquals(65536u, (list[2] as CborInt).value) + assertEquals(1u, (list[0] as CborInteger).value) + assertEquals(255u, (list[1] as CborInteger).value) + assertEquals(65536u, (list[2] as CborInteger).value) } @Test @@ -763,7 +763,7 @@ data class MixedBag( val bStr: CborByteString?, val cborElement: CborElement?, val cborPositiveInt: CborPrimitive, - val cborInt: CborInt, + val cborInt: CborInteger, @KeyTags(42u) @ValueTags(2337u) val tagged: Int From 0949c77c5d69d9a04809a6076ecfdc84b5d38b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 24 Jan 2026 23:19:56 +0100 Subject: [PATCH 54/68] Rename @CborArray to @CborObjectAsArray --- docs/formats.md | 2 +- .../cbor/api/kotlinx-serialization-cbor.api | 41 ++++++------ .../api/kotlinx-serialization-cbor.klib.api | 67 +++++++++---------- .../{CborArray.kt => CborObjectAsArray.kt} | 4 +- .../src/kotlinx/serialization/cbor/Tags.kt | 2 +- .../serialization/cbor/internal/Encoding.kt | 2 +- .../serialization/cbor/CborArrayTest.kt | 6 +- 7 files changed, 61 insertions(+), 63 deletions(-) rename formats/cbor/commonMain/src/kotlinx/serialization/cbor/{CborArray.kt => CborObjectAsArray.kt} (93%) diff --git a/docs/formats.md b/docs/formats.md index 934201cf35..2557aeea37 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -305,7 +305,7 @@ A2 # map(2) F6 # primitive(22) ``` -When annotated with `@CborArray`, serialization of the same object will produce a Cbor array: bytes `0x8226F6`, or in diagnostic notation: +When annotated with `@CborObjectAsArray`, serialization of the same object will produce a Cbor array: bytes `0x8226F6`, or in diagnostic notation: ``` 82 # array(2) diff --git a/formats/cbor/api/kotlinx-serialization-cbor.api b/formats/cbor/api/kotlinx-serialization-cbor.api index 15373025c7..5c1df4ad92 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.api @@ -20,13 +20,6 @@ public final class kotlinx/serialization/cbor/Cbor$Default : kotlinx/serializati public final fun getCoseCompliant ()Lkotlinx/serialization/cbor/Cbor; } -public abstract interface annotation class kotlinx/serialization/cbor/CborArray : java/lang/annotation/Annotation { -} - -public final synthetic class kotlinx/serialization/cbor/CborArray$Impl : kotlinx/serialization/cbor/CborArray { - public fun ()V -} - public final class kotlinx/serialization/cbor/CborBoolean : kotlinx/serialization/cbor/CborPrimitive { public static final field Companion Lkotlinx/serialization/cbor/CborBoolean$Companion; public synthetic fun (Z[JLkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -118,16 +111,16 @@ public final class kotlinx/serialization/cbor/CborElement$Companion { } public final class kotlinx/serialization/cbor/CborElementKt { - public static final fun CborInt-SIFponk (J[J)Lkotlinx/serialization/cbor/CborInt; - public static final fun CborInt-ahITK_k (J[J)Lkotlinx/serialization/cbor/CborInt; - public static final fun getByte (Lkotlinx/serialization/cbor/CborInt;)B - public static final fun getByteOrNull (Lkotlinx/serialization/cbor/CborInt;)Ljava/lang/Byte; - public static final fun getInt (Lkotlinx/serialization/cbor/CborInt;)I - public static final fun getIntOrNull (Lkotlinx/serialization/cbor/CborInt;)Ljava/lang/Integer; - public static final fun getLong (Lkotlinx/serialization/cbor/CborInt;)J - public static final fun getLongOrNull (Lkotlinx/serialization/cbor/CborInt;)Ljava/lang/Long; - public static final fun getShort (Lkotlinx/serialization/cbor/CborInt;)S - public static final fun getShortOrNull (Lkotlinx/serialization/cbor/CborInt;)Ljava/lang/Short; + public static final fun CborInt-SIFponk (J[J)Lkotlinx/serialization/cbor/CborInteger; + public static final fun CborInt-ahITK_k (J[J)Lkotlinx/serialization/cbor/CborInteger; + public static final fun getByte (Lkotlinx/serialization/cbor/CborInteger;)B + public static final fun getByteOrNull (Lkotlinx/serialization/cbor/CborInteger;)Ljava/lang/Byte; + public static final fun getInt (Lkotlinx/serialization/cbor/CborInteger;)I + public static final fun getIntOrNull (Lkotlinx/serialization/cbor/CborInteger;)Ljava/lang/Integer; + public static final fun getLong (Lkotlinx/serialization/cbor/CborInteger;)J + public static final fun getLongOrNull (Lkotlinx/serialization/cbor/CborInteger;)Ljava/lang/Long; + public static final fun getShort (Lkotlinx/serialization/cbor/CborInteger;)S + public static final fun getShortOrNull (Lkotlinx/serialization/cbor/CborInteger;)Ljava/lang/Short; } public abstract interface class kotlinx/serialization/cbor/CborEncoder : kotlinx/serialization/encoding/Encoder { @@ -159,19 +152,18 @@ public final class kotlinx/serialization/cbor/CborFloat$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class kotlinx/serialization/cbor/CborInt : kotlinx/serialization/cbor/CborPrimitive { - public static final field Companion Lkotlinx/serialization/cbor/CborInt$Companion; +public final class kotlinx/serialization/cbor/CborInteger : kotlinx/serialization/cbor/CborPrimitive { + public static final field Companion Lkotlinx/serialization/cbor/CborInteger$Companion; public synthetic fun (JZ[JLkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z public synthetic fun getValue ()Ljava/lang/Object; public fun getValue-s-VKNKU ()J public fun hashCode ()I public final fun isPositive ()Z - public final fun toLong ()J public fun toString ()Ljava/lang/String; } -public final class kotlinx/serialization/cbor/CborInt$Companion { +public final class kotlinx/serialization/cbor/CborInteger$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } @@ -309,6 +301,13 @@ public final synthetic class kotlinx/serialization/cbor/CborNullAsEmptyMap$Impl public fun ()V } +public abstract interface annotation class kotlinx/serialization/cbor/CborObjectAsArray : java/lang/annotation/Annotation { +} + +public final synthetic class kotlinx/serialization/cbor/CborObjectAsArray$Impl : kotlinx/serialization/cbor/CborObjectAsArray { + public fun ()V +} + public abstract class kotlinx/serialization/cbor/CborPrimitive : kotlinx/serialization/cbor/CborElement { public static final field Companion Lkotlinx/serialization/cbor/CborPrimitive$Companion; public synthetic fun ([JILkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/formats/cbor/api/kotlinx-serialization-cbor.klib.api b/formats/cbor/api/kotlinx-serialization-cbor.klib.api index eb198d2495..5d373b50b0 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.klib.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.klib.api @@ -10,10 +10,6 @@ open annotation class kotlinx.serialization.cbor/ByteString : kotlin/Annotation constructor () // kotlinx.serialization.cbor/ByteString.|(){}[0] } -open annotation class kotlinx.serialization.cbor/CborArray : kotlin/Annotation { // kotlinx.serialization.cbor/CborArray|null[0] - constructor () // kotlinx.serialization.cbor/CborArray.|(){}[0] -} - open annotation class kotlinx.serialization.cbor/CborLabel : kotlin/Annotation { // kotlinx.serialization.cbor/CborLabel|null[0] constructor (kotlin/Long) // kotlinx.serialization.cbor/CborLabel.|(kotlin.Long){}[0] @@ -25,6 +21,10 @@ open annotation class kotlinx.serialization.cbor/CborNullAsEmptyMap : kotlin/Ann constructor () // kotlinx.serialization.cbor/CborNullAsEmptyMap.|(){}[0] } +open annotation class kotlinx.serialization.cbor/CborObjectAsArray : kotlin/Annotation { // kotlinx.serialization.cbor/CborObjectAsArray|null[0] + constructor () // kotlinx.serialization.cbor/CborObjectAsArray.|(){}[0] +} + open annotation class kotlinx.serialization.cbor/KeyTags : kotlin/Annotation { // kotlinx.serialization.cbor/KeyTags|null[0] constructor (kotlin/ULongArray...) // kotlinx.serialization.cbor/KeyTags.|(kotlin.ULongArray...){}[0] @@ -168,21 +168,20 @@ final class kotlinx.serialization.cbor/CborFloat : kotlinx.serialization.cbor/Cb } } -final class kotlinx.serialization.cbor/CborInt : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborInt|null[0] - constructor (kotlin/ULong, kotlin/Boolean, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborInt.|(kotlin.ULong;kotlin.Boolean;kotlin.ULongArray...){}[0] +final class kotlinx.serialization.cbor/CborInteger : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborInteger|null[0] + constructor (kotlin/ULong, kotlin/Boolean, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborInteger.|(kotlin.ULong;kotlin.Boolean;kotlin.ULongArray...){}[0] - final val isPositive // kotlinx.serialization.cbor/CborInt.isPositive|{}isPositive[0] - final fun (): kotlin/Boolean // kotlinx.serialization.cbor/CborInt.isPositive.|(){}[0] - final val value // kotlinx.serialization.cbor/CborInt.value|{}value[0] - final fun (): kotlin/ULong // kotlinx.serialization.cbor/CborInt.value.|(){}[0] + final val isPositive // kotlinx.serialization.cbor/CborInteger.isPositive|{}isPositive[0] + final fun (): kotlin/Boolean // kotlinx.serialization.cbor/CborInteger.isPositive.|(){}[0] + final val value // kotlinx.serialization.cbor/CborInteger.value|{}value[0] + final fun (): kotlin/ULong // kotlinx.serialization.cbor/CborInteger.value.|(){}[0] - final fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborInt.equals|equals(kotlin.Any?){}[0] - final fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborInt.hashCode|hashCode(){}[0] - final fun toLong(): kotlin/Long // kotlinx.serialization.cbor/CborInt.toLong|toLong(){}[0] - final fun toString(): kotlin/String // kotlinx.serialization.cbor/CborInt.toString|toString(){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborInteger.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborInteger.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // kotlinx.serialization.cbor/CborInteger.toString|toString(){}[0] - final object Companion { // kotlinx.serialization.cbor/CborInt.Companion|null[0] - final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborInt.Companion.serializer|serializer(){}[0] + final object Companion { // kotlinx.serialization.cbor/CborInteger.Companion|null[0] + final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborInteger.Companion.serializer|serializer(){}[0] } } @@ -360,25 +359,25 @@ final object kotlinx.serialization.cbor/CborTag { // kotlinx.serialization.cbor/ final fun (): kotlin/ULong // kotlinx.serialization.cbor/CborTag.URI.|(){}[0] } -final val kotlinx.serialization.cbor/byte // kotlinx.serialization.cbor/byte|@kotlinx.serialization.cbor.CborInt{}byte[0] - final fun (kotlinx.serialization.cbor/CborInt).(): kotlin/Byte // kotlinx.serialization.cbor/byte.|@kotlinx.serialization.cbor.CborInt(){}[0] -final val kotlinx.serialization.cbor/byteOrNull // kotlinx.serialization.cbor/byteOrNull|@kotlinx.serialization.cbor.CborInt{}byteOrNull[0] - final fun (kotlinx.serialization.cbor/CborInt).(): kotlin/Byte? // kotlinx.serialization.cbor/byteOrNull.|@kotlinx.serialization.cbor.CborInt(){}[0] -final val kotlinx.serialization.cbor/int // kotlinx.serialization.cbor/int|@kotlinx.serialization.cbor.CborInt{}int[0] - final fun (kotlinx.serialization.cbor/CborInt).(): kotlin/Int // kotlinx.serialization.cbor/int.|@kotlinx.serialization.cbor.CborInt(){}[0] -final val kotlinx.serialization.cbor/intOrNull // kotlinx.serialization.cbor/intOrNull|@kotlinx.serialization.cbor.CborInt{}intOrNull[0] - final fun (kotlinx.serialization.cbor/CborInt).(): kotlin/Int? // kotlinx.serialization.cbor/intOrNull.|@kotlinx.serialization.cbor.CborInt(){}[0] -final val kotlinx.serialization.cbor/long // kotlinx.serialization.cbor/long|@kotlinx.serialization.cbor.CborInt{}long[0] - final fun (kotlinx.serialization.cbor/CborInt).(): kotlin/Long // kotlinx.serialization.cbor/long.|@kotlinx.serialization.cbor.CborInt(){}[0] -final val kotlinx.serialization.cbor/longOrNull // kotlinx.serialization.cbor/longOrNull|@kotlinx.serialization.cbor.CborInt{}longOrNull[0] - final fun (kotlinx.serialization.cbor/CborInt).(): kotlin/Long? // kotlinx.serialization.cbor/longOrNull.|@kotlinx.serialization.cbor.CborInt(){}[0] -final val kotlinx.serialization.cbor/short // kotlinx.serialization.cbor/short|@kotlinx.serialization.cbor.CborInt{}short[0] - final fun (kotlinx.serialization.cbor/CborInt).(): kotlin/Short // kotlinx.serialization.cbor/short.|@kotlinx.serialization.cbor.CborInt(){}[0] -final val kotlinx.serialization.cbor/shortOrNull // kotlinx.serialization.cbor/shortOrNull|@kotlinx.serialization.cbor.CborInt{}shortOrNull[0] - final fun (kotlinx.serialization.cbor/CborInt).(): kotlin/Short? // kotlinx.serialization.cbor/shortOrNull.|@kotlinx.serialization.cbor.CborInt(){}[0] +final val kotlinx.serialization.cbor/byte // kotlinx.serialization.cbor/byte|@kotlinx.serialization.cbor.CborInteger{}byte[0] + final fun (kotlinx.serialization.cbor/CborInteger).(): kotlin/Byte // kotlinx.serialization.cbor/byte.|@kotlinx.serialization.cbor.CborInteger(){}[0] +final val kotlinx.serialization.cbor/byteOrNull // kotlinx.serialization.cbor/byteOrNull|@kotlinx.serialization.cbor.CborInteger{}byteOrNull[0] + final fun (kotlinx.serialization.cbor/CborInteger).(): kotlin/Byte? // kotlinx.serialization.cbor/byteOrNull.|@kotlinx.serialization.cbor.CborInteger(){}[0] +final val kotlinx.serialization.cbor/int // kotlinx.serialization.cbor/int|@kotlinx.serialization.cbor.CborInteger{}int[0] + final fun (kotlinx.serialization.cbor/CborInteger).(): kotlin/Int // kotlinx.serialization.cbor/int.|@kotlinx.serialization.cbor.CborInteger(){}[0] +final val kotlinx.serialization.cbor/intOrNull // kotlinx.serialization.cbor/intOrNull|@kotlinx.serialization.cbor.CborInteger{}intOrNull[0] + final fun (kotlinx.serialization.cbor/CborInteger).(): kotlin/Int? // kotlinx.serialization.cbor/intOrNull.|@kotlinx.serialization.cbor.CborInteger(){}[0] +final val kotlinx.serialization.cbor/long // kotlinx.serialization.cbor/long|@kotlinx.serialization.cbor.CborInteger{}long[0] + final fun (kotlinx.serialization.cbor/CborInteger).(): kotlin/Long // kotlinx.serialization.cbor/long.|@kotlinx.serialization.cbor.CborInteger(){}[0] +final val kotlinx.serialization.cbor/longOrNull // kotlinx.serialization.cbor/longOrNull|@kotlinx.serialization.cbor.CborInteger{}longOrNull[0] + final fun (kotlinx.serialization.cbor/CborInteger).(): kotlin/Long? // kotlinx.serialization.cbor/longOrNull.|@kotlinx.serialization.cbor.CborInteger(){}[0] +final val kotlinx.serialization.cbor/short // kotlinx.serialization.cbor/short|@kotlinx.serialization.cbor.CborInteger{}short[0] + final fun (kotlinx.serialization.cbor/CborInteger).(): kotlin/Short // kotlinx.serialization.cbor/short.|@kotlinx.serialization.cbor.CborInteger(){}[0] +final val kotlinx.serialization.cbor/shortOrNull // kotlinx.serialization.cbor/shortOrNull|@kotlinx.serialization.cbor.CborInteger{}shortOrNull[0] + final fun (kotlinx.serialization.cbor/CborInteger).(): kotlin/Short? // kotlinx.serialization.cbor/shortOrNull.|@kotlinx.serialization.cbor.CborInteger(){}[0] final fun kotlinx.serialization.cbor/Cbor(kotlinx.serialization.cbor/Cbor = ..., kotlin/Function1): kotlinx.serialization.cbor/Cbor // kotlinx.serialization.cbor/Cbor|Cbor(kotlinx.serialization.cbor.Cbor;kotlin.Function1){}[0] -final fun kotlinx.serialization.cbor/CborInt(kotlin/Long, kotlin/ULongArray...): kotlinx.serialization.cbor/CborInt // kotlinx.serialization.cbor/CborInt|CborInt(kotlin.Long;kotlin.ULongArray...){}[0] -final fun kotlinx.serialization.cbor/CborInt(kotlin/ULong, kotlin/ULongArray...): kotlinx.serialization.cbor/CborInt // kotlinx.serialization.cbor/CborInt|CborInt(kotlin.ULong;kotlin.ULongArray...){}[0] +final fun kotlinx.serialization.cbor/CborInt(kotlin/Long, kotlin/ULongArray...): kotlinx.serialization.cbor/CborInteger // kotlinx.serialization.cbor/CborInt|CborInt(kotlin.Long;kotlin.ULongArray...){}[0] +final fun kotlinx.serialization.cbor/CborInt(kotlin/ULong, kotlin/ULongArray...): kotlinx.serialization.cbor/CborInteger // kotlinx.serialization.cbor/CborInt|CborInt(kotlin.ULong;kotlin.ULongArray...){}[0] final inline fun <#A: reified kotlin/Any?> (kotlinx.serialization.cbor/Cbor).kotlinx.serialization.cbor/decodeFromCborElement(kotlinx.serialization.cbor/CborElement): #A // kotlinx.serialization.cbor/decodeFromCborElement|decodeFromCborElement@kotlinx.serialization.cbor.Cbor(kotlinx.serialization.cbor.CborElement){0§}[0] final inline fun <#A: reified kotlin/Any?> (kotlinx.serialization.cbor/Cbor).kotlinx.serialization.cbor/encodeToCborElement(#A): kotlinx.serialization.cbor/CborElement // kotlinx.serialization.cbor/encodeToCborElement|encodeToCborElement@kotlinx.serialization.cbor.Cbor(0:0){0§}[0] diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborArray.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborObjectAsArray.kt similarity index 93% rename from formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborArray.kt rename to formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborObjectAsArray.kt index 9727b606ba..41d33ac12e 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborArray.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborObjectAsArray.kt @@ -11,7 +11,7 @@ import kotlinx.serialization.* * Example usage: * * ``` - * @CborArray + * @CborObjectAsArray * @Serializable * data class DataClass( * val alg: Int, @@ -36,4 +36,4 @@ import kotlinx.serialization.* @SerialInfo @Target(AnnotationTarget.CLASS) @ExperimentalSerializationApi -public annotation class CborArray +public annotation class CborObjectAsArray diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Tags.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Tags.kt index 81ee763b49..80a1a5f72c 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Tags.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Tags.kt @@ -83,7 +83,7 @@ public annotation class KeyTags(@OptIn(ExperimentalUnsignedTypes::class) vararg /** * Specifies that an object of a class annotated using `ObjectTags` shall be tagged and serialized as - * CBOR major type 6: optional semantic tagging of other major types. Can be combined with [CborArray] and [ValueTags]. + * CBOR major type 6: optional semantic tagging of other major types. Can be combined with [CborObjectAsArray] and [ValueTags]. * Note that `ObjectTags` will always be encoded directly before to the data of the tagged object, i.e. a value-tagged * property of an object-tagged type will have the value tags preceding the object tags. * diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoding.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoding.kt index 2e32d2aff6..bf4fd9de10 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoding.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoding.kt @@ -71,7 +71,7 @@ internal fun SerialDescriptor.getCborLabel(index: Int): Long? = findAnnotation Date: Sat, 24 Jan 2026 23:29:51 +0100 Subject: [PATCH 55/68] Rename CborList -> CborArray --- docs/formats.md | 7 +- .../cbor/api/kotlinx-serialization-cbor.api | 92 +++++++++---------- .../api/kotlinx-serialization-cbor.klib.api | 56 +++++------ .../kotlinx/serialization/cbor/CborElement.kt | 12 +-- .../kotlinx/serialization/cbor/CborEncoder.kt | 2 +- .../cbor/internal/CborElementSerializers.kt | 16 ++-- .../cbor/internal/CborParserInterface.kt | 1 - .../cbor/internal/CborTreeReader.kt | 4 +- .../serialization/cbor/internal/Decoder.kt | 12 +-- .../serialization/cbor/internal/Encoder.kt | 2 +- .../cbor/CborElementEqualityTest.kt | 26 +++--- .../cbor/CborElementSerializerBehaviorTest.kt | 10 +- .../serialization/cbor/CborElementTest.kt | 30 +++--- 13 files changed, 134 insertions(+), 136 deletions(-) diff --git a/docs/formats.md b/docs/formats.md index 2557aeea37..2b0449e3aa 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -430,7 +430,7 @@ A [CborElement] class has three direct subtypes, closely following CBOR grammar: * [CborByteString] maps to a Kotlin `ByteArray` and is used to encode them as CBOR byte string (in contrast to a list of individual bytes) -* [CborList] represents a CBOR array. It is a Kotlin `List` of `CborElement` items. +* [CborArray] represents a CBOR array. It is a Kotlin `List` of `CborElement` items. * [CborMap] represents a CBOR map/object. It is a Kotlin `Map` from `CborElement` keys to `CborElement` values. This is typically the result of serializing an arbitrary @@ -1797,14 +1797,13 @@ This chapter concludes [Kotlin Serialization Guide](serialization-guide.md). [CborElement]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-element/index.html [Cbor.encodeToCborElement]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/encode-to-cbor-element.html [CborPrimitive]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-primitive/index.html -[CborPrimitive.value]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-primitive/value.html [CborNull]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-null/index.html [CborBoolean]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-boolean/index.html -[CborInt]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-int/index.html +[CborInt]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-int.html [CborString]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-string/index.html [CborFloat]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-float/index.html [CborByteString]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-byte-string/index.html -[CborList]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-list/index.html +[CborArray]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-array/index.html [CborMap]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-map/index.html diff --git a/formats/cbor/api/kotlinx-serialization-cbor.api b/formats/cbor/api/kotlinx-serialization-cbor.api index 5c1df4ad92..e27bdb52ea 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.api @@ -20,6 +20,52 @@ public final class kotlinx/serialization/cbor/Cbor$Default : kotlinx/serializati public final fun getCoseCompliant ()Lkotlinx/serialization/cbor/Cbor; } +public final class kotlinx/serialization/cbor/CborArray : kotlinx/serialization/cbor/CborElement, java/util/List, kotlin/jvm/internal/markers/KMappedMarker { + public static final field Companion Lkotlinx/serialization/cbor/CborArray$Companion; + public synthetic fun (Ljava/util/List;[JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun add (ILjava/lang/Object;)V + public fun add (ILkotlinx/serialization/cbor/CborElement;)V + public synthetic fun add (Ljava/lang/Object;)Z + public fun add (Lkotlinx/serialization/cbor/CborElement;)Z + public fun addAll (ILjava/util/Collection;)Z + public fun addAll (Ljava/util/Collection;)Z + public fun clear ()V + public final fun contains (Ljava/lang/Object;)Z + public fun contains (Lkotlinx/serialization/cbor/CborElement;)Z + public fun containsAll (Ljava/util/Collection;)Z + public fun equals (Ljava/lang/Object;)Z + public synthetic fun get (I)Ljava/lang/Object; + public fun get (I)Lkotlinx/serialization/cbor/CborElement; + public fun getSize ()I + public fun hashCode ()I + public final fun indexOf (Ljava/lang/Object;)I + public fun indexOf (Lkotlinx/serialization/cbor/CborElement;)I + public fun isEmpty ()Z + public fun iterator ()Ljava/util/Iterator; + public final fun lastIndexOf (Ljava/lang/Object;)I + public fun lastIndexOf (Lkotlinx/serialization/cbor/CborElement;)I + public fun listIterator ()Ljava/util/ListIterator; + public fun listIterator (I)Ljava/util/ListIterator; + public synthetic fun remove (I)Ljava/lang/Object; + public fun remove (I)Lkotlinx/serialization/cbor/CborElement; + public fun remove (Ljava/lang/Object;)Z + public fun removeAll (Ljava/util/Collection;)Z + public fun replaceAll (Ljava/util/function/UnaryOperator;)V + public fun retainAll (Ljava/util/Collection;)Z + public synthetic fun set (ILjava/lang/Object;)Ljava/lang/Object; + public fun set (ILkotlinx/serialization/cbor/CborElement;)Lkotlinx/serialization/cbor/CborElement; + public final fun size ()I + public fun sort (Ljava/util/Comparator;)V + public fun subList (II)Ljava/util/List; + public fun toArray ()[Ljava/lang/Object; + public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; + public fun toString ()Ljava/lang/String; +} + +public final class kotlinx/serialization/cbor/CborArray$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class kotlinx/serialization/cbor/CborBoolean : kotlinx/serialization/cbor/CborPrimitive { public static final field Companion Lkotlinx/serialization/cbor/CborBoolean$Companion; public synthetic fun (Z[JLkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -181,52 +227,6 @@ public final synthetic class kotlinx/serialization/cbor/CborLabel$Impl : kotlinx public final synthetic fun label ()J } -public final class kotlinx/serialization/cbor/CborList : kotlinx/serialization/cbor/CborElement, java/util/List, kotlin/jvm/internal/markers/KMappedMarker { - public static final field Companion Lkotlinx/serialization/cbor/CborList$Companion; - public synthetic fun (Ljava/util/List;[JLkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun add (ILjava/lang/Object;)V - public fun add (ILkotlinx/serialization/cbor/CborElement;)V - public synthetic fun add (Ljava/lang/Object;)Z - public fun add (Lkotlinx/serialization/cbor/CborElement;)Z - public fun addAll (ILjava/util/Collection;)Z - public fun addAll (Ljava/util/Collection;)Z - public fun clear ()V - public final fun contains (Ljava/lang/Object;)Z - public fun contains (Lkotlinx/serialization/cbor/CborElement;)Z - public fun containsAll (Ljava/util/Collection;)Z - public fun equals (Ljava/lang/Object;)Z - public synthetic fun get (I)Ljava/lang/Object; - public fun get (I)Lkotlinx/serialization/cbor/CborElement; - public fun getSize ()I - public fun hashCode ()I - public final fun indexOf (Ljava/lang/Object;)I - public fun indexOf (Lkotlinx/serialization/cbor/CborElement;)I - public fun isEmpty ()Z - public fun iterator ()Ljava/util/Iterator; - public final fun lastIndexOf (Ljava/lang/Object;)I - public fun lastIndexOf (Lkotlinx/serialization/cbor/CborElement;)I - public fun listIterator ()Ljava/util/ListIterator; - public fun listIterator (I)Ljava/util/ListIterator; - public synthetic fun remove (I)Ljava/lang/Object; - public fun remove (I)Lkotlinx/serialization/cbor/CborElement; - public fun remove (Ljava/lang/Object;)Z - public fun removeAll (Ljava/util/Collection;)Z - public fun replaceAll (Ljava/util/function/UnaryOperator;)V - public fun retainAll (Ljava/util/Collection;)Z - public synthetic fun set (ILjava/lang/Object;)Ljava/lang/Object; - public fun set (ILkotlinx/serialization/cbor/CborElement;)Lkotlinx/serialization/cbor/CborElement; - public final fun size ()I - public fun sort (Ljava/util/Comparator;)V - public fun subList (II)Ljava/util/List; - public fun toArray ()[Ljava/lang/Object; - public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; - public fun toString ()Ljava/lang/String; -} - -public final class kotlinx/serialization/cbor/CborList$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - public final class kotlinx/serialization/cbor/CborMap : kotlinx/serialization/cbor/CborElement, java/util/Map, kotlin/jvm/internal/markers/KMappedMarker { public static final field Companion Lkotlinx/serialization/cbor/CborMap$Companion; public synthetic fun (Ljava/util/Map;[JLkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/formats/cbor/api/kotlinx-serialization-cbor.klib.api b/formats/cbor/api/kotlinx-serialization-cbor.klib.api index 5d373b50b0..3377428921 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.klib.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.klib.api @@ -65,6 +65,34 @@ abstract interface kotlinx.serialization.cbor/CborEncoder : kotlinx.serializatio open fun encodeCborElement(kotlinx.serialization.cbor/CborElement) // kotlinx.serialization.cbor/CborEncoder.encodeCborElement|encodeCborElement(kotlinx.serialization.cbor.CborElement){}[0] } +final class kotlinx.serialization.cbor/CborArray : kotlin.collections/List, kotlinx.serialization.cbor/CborElement { // kotlinx.serialization.cbor/CborArray|null[0] + constructor (kotlin.collections/List, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborArray.|(kotlin.collections.List;kotlin.ULongArray...){}[0] + + final val size // kotlinx.serialization.cbor/CborArray.size|{}size[0] + final fun (): kotlin/Int // kotlinx.serialization.cbor/CborArray.size.|(){}[0] + + final fun contains(kotlinx.serialization.cbor/CborElement): kotlin/Boolean // kotlinx.serialization.cbor/CborArray.contains|contains(kotlinx.serialization.cbor.CborElement){}[0] + final fun containsAll(kotlin.collections/Collection): kotlin/Boolean // kotlinx.serialization.cbor/CborArray.containsAll|containsAll(kotlin.collections.Collection){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborArray.equals|equals(kotlin.Any?){}[0] + final fun get(kotlin/Int): kotlinx.serialization.cbor/CborElement // kotlinx.serialization.cbor/CborArray.get|get(kotlin.Int){}[0] + final fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborArray.hashCode|hashCode(){}[0] + final fun indexOf(kotlinx.serialization.cbor/CborElement): kotlin/Int // kotlinx.serialization.cbor/CborArray.indexOf|indexOf(kotlinx.serialization.cbor.CborElement){}[0] + final fun isEmpty(): kotlin/Boolean // kotlinx.serialization.cbor/CborArray.isEmpty|isEmpty(){}[0] + final fun iterator(): kotlin.collections/Iterator // kotlinx.serialization.cbor/CborArray.iterator|iterator(){}[0] + final fun lastIndexOf(kotlinx.serialization.cbor/CborElement): kotlin/Int // kotlinx.serialization.cbor/CborArray.lastIndexOf|lastIndexOf(kotlinx.serialization.cbor.CborElement){}[0] + final fun listIterator(): kotlin.collections/ListIterator // kotlinx.serialization.cbor/CborArray.listIterator|listIterator(){}[0] + final fun listIterator(kotlin/Int): kotlin.collections/ListIterator // kotlinx.serialization.cbor/CborArray.listIterator|listIterator(kotlin.Int){}[0] + final fun subList(kotlin/Int, kotlin/Int): kotlin.collections/List // kotlinx.serialization.cbor/CborArray.subList|subList(kotlin.Int;kotlin.Int){}[0] + final fun toString(): kotlin/String // kotlinx.serialization.cbor/CborArray.toString|toString(){}[0] + + final object Companion { // kotlinx.serialization.cbor/CborArray.Companion|null[0] + final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborArray.Companion.serializer|serializer(){}[0] + } + + // Targets: [js] + final fun asJsReadonlyArrayView(): kotlin.js.collections/JsReadonlyArray // kotlinx.serialization.cbor/CborArray.asJsReadonlyArrayView|asJsReadonlyArrayView(){}[0] +} + final class kotlinx.serialization.cbor/CborBoolean : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborBoolean|null[0] constructor (kotlin/Boolean, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborBoolean.|(kotlin.Boolean;kotlin.ULongArray...){}[0] @@ -185,34 +213,6 @@ final class kotlinx.serialization.cbor/CborInteger : kotlinx.serialization.cbor/ } } -final class kotlinx.serialization.cbor/CborList : kotlin.collections/List, kotlinx.serialization.cbor/CborElement { // kotlinx.serialization.cbor/CborList|null[0] - constructor (kotlin.collections/List, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborList.|(kotlin.collections.List;kotlin.ULongArray...){}[0] - - final val size // kotlinx.serialization.cbor/CborList.size|{}size[0] - final fun (): kotlin/Int // kotlinx.serialization.cbor/CborList.size.|(){}[0] - - final fun contains(kotlinx.serialization.cbor/CborElement): kotlin/Boolean // kotlinx.serialization.cbor/CborList.contains|contains(kotlinx.serialization.cbor.CborElement){}[0] - final fun containsAll(kotlin.collections/Collection): kotlin/Boolean // kotlinx.serialization.cbor/CborList.containsAll|containsAll(kotlin.collections.Collection){}[0] - final fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborList.equals|equals(kotlin.Any?){}[0] - final fun get(kotlin/Int): kotlinx.serialization.cbor/CborElement // kotlinx.serialization.cbor/CborList.get|get(kotlin.Int){}[0] - final fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborList.hashCode|hashCode(){}[0] - final fun indexOf(kotlinx.serialization.cbor/CborElement): kotlin/Int // kotlinx.serialization.cbor/CborList.indexOf|indexOf(kotlinx.serialization.cbor.CborElement){}[0] - final fun isEmpty(): kotlin/Boolean // kotlinx.serialization.cbor/CborList.isEmpty|isEmpty(){}[0] - final fun iterator(): kotlin.collections/Iterator // kotlinx.serialization.cbor/CborList.iterator|iterator(){}[0] - final fun lastIndexOf(kotlinx.serialization.cbor/CborElement): kotlin/Int // kotlinx.serialization.cbor/CborList.lastIndexOf|lastIndexOf(kotlinx.serialization.cbor.CborElement){}[0] - final fun listIterator(): kotlin.collections/ListIterator // kotlinx.serialization.cbor/CborList.listIterator|listIterator(){}[0] - final fun listIterator(kotlin/Int): kotlin.collections/ListIterator // kotlinx.serialization.cbor/CborList.listIterator|listIterator(kotlin.Int){}[0] - final fun subList(kotlin/Int, kotlin/Int): kotlin.collections/List // kotlinx.serialization.cbor/CborList.subList|subList(kotlin.Int;kotlin.Int){}[0] - final fun toString(): kotlin/String // kotlinx.serialization.cbor/CborList.toString|toString(){}[0] - - final object Companion { // kotlinx.serialization.cbor/CborList.Companion|null[0] - final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborList.Companion.serializer|serializer(){}[0] - } - - // Targets: [js] - final fun asJsReadonlyArrayView(): kotlin.js.collections/JsReadonlyArray // kotlinx.serialization.cbor/CborList.asJsReadonlyArrayView|asJsReadonlyArrayView(){}[0] -} - final class kotlinx.serialization.cbor/CborMap : kotlin.collections/Map, kotlinx.serialization.cbor/CborElement { // kotlinx.serialization.cbor/CborMap|null[0] constructor (kotlin.collections/Map, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborMap.|(kotlin.collections.Map;kotlin.ULongArray...){}[0] diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt index 5c245e6db5..ee54dfb146 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -15,7 +15,7 @@ internal val EMPTY_TAGS: ULongArray = ULongArray(0) /** * Class representing single CBOR element. - * Can be [CborPrimitive], [CborMap] or [CborList]. + * Can be [CborPrimitive], [CborMap] or [CborArray]. * * [CborElement.toString] properly prints CBOR tree as a human-readable representation. * Whole hierarchy is serializable, but only when used with [Cbor] as [CborElement] is purely CBOR-specific structure @@ -357,25 +357,25 @@ public class CborMap( } /** - * Class representing CBOR array, consisting of CBOR elements. + * Class representing CBOR array consisting of CBOR elements. * * Since this class also implements [List] interface, you can use * traditional methods like [List.get] or [List.size] to obtain CBOR elements. */ -@Serializable(with = CborListSerializer::class) +@Serializable(with = CborArraySerializer::class) @ExperimentalSerializationApi -public class CborList( +public class CborArray( private val content: List, vararg tags: ULong ) : CborElement(tags), List by content { public override fun equals(other: Any?): Boolean = - other is CborList && other.content == content && other.tags.contentEquals(tags) + other is CborArray && other.content == content && other.tags.contentEquals(tags) public override fun hashCode(): Int = content.hashCode() * 31 + tags.contentHashCode() override fun toString(): String { - return "CborList(" + + return "CborArray(" + "tags=${tags.joinToString(prefix = "[", postfix = "]")}, " + "content=$content" + ")" diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt index 60c1f09db7..1f181d3717 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt @@ -52,7 +52,7 @@ public interface CborEncoder : Encoder { * fun serialize(encoder: Encoder, value: Holder) { * val composite = encoder.beginStructure(descriptor) * composite.encodeSerializableElement(descriptor, 0, Int.serializer(), value.value) - * val array = CborList(value.list.map { CborInt(it.toLong()) }) + * val array = CborArray(value.list.map { CborInt(it.toLong()) }) * // Incorrect, encoder is already in an intermediate state after encodeSerializableElement * (composite as CborEncoder).encodeCborElement(array) * composite.endStructure(descriptor) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index 6aa5197c4d..26e4b67e32 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -28,7 +28,7 @@ internal object CborElementSerializer : KSerializer, CborSerializer element("CborBoolean", defer { CborBooleanSerializer.descriptor }) element("CborByteString", defer { CborByteStringSerializer.descriptor }) element("CborMap", defer { CborMapSerializer.descriptor }) - element("CborList", defer { CborListSerializer.descriptor }) + element("CborArray", defer { CborArraySerializer.descriptor }) element("CborDouble", defer { CborFloatSerializer.descriptor }) element("CborInt", defer { CborIntSerializer.descriptor }) } @@ -40,7 +40,7 @@ internal object CborElementSerializer : KSerializer, CborSerializer when (value) { is CborPrimitive -> encoder.encodeSerializableValue(CborPrimitiveSerializer, value) is CborMap -> encoder.encodeSerializableValue(CborMapSerializer, value) - is CborList -> encoder.encodeSerializableValue(CborListSerializer, value) + is CborArray -> encoder.encodeSerializableValue(CborArraySerializer, value) } } @@ -247,25 +247,25 @@ internal object CborMapSerializer : KSerializer, CborSerializer { } /** - * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborList]. + * Serializer object providing [SerializationStrategy] and [DeserializationStrategy] for [CborArray]. * It can only be used by with [Cbor] format an its input ([CborDecoder] and [CborEncoder]). */ -internal object CborListSerializer : KSerializer, CborSerializer { +internal object CborArraySerializer : KSerializer, CborSerializer { override val descriptor: SerialDescriptor = SerialDescriptor( - serialName = "kotlinx.serialization.cbor.CborList", + serialName = "kotlinx.serialization.cbor.CborArray", original = ListSerializer(CborElementSerializer).descriptor ) - override fun serialize(encoder: Encoder, value: CborList) { + override fun serialize(encoder: Encoder, value: CborArray) { val cborEncoder = encoder.asCborEncoder() cborEncoder.encodeTags(value.tags) ListSerializer(CborElementSerializer).serialize(encoder, value) } - override fun deserialize(decoder: Decoder): CborList { + override fun deserialize(decoder: Decoder): CborArray { val element = decoder.asCborDecoder().decodeCborElement() - if (element !is CborList) throw CborDecodingException("Unexpected CBOR element, expected CborList, had ${element::class}") + if (element !is CborArray) throw CborDecodingException("Unexpected CBOR element, expected CborArray, had ${element::class}") return element } } diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt index f3f4b2fbb2..970dafa99e 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt @@ -6,7 +6,6 @@ package kotlinx.serialization.cbor.internal import kotlinx.serialization.* -import kotlinx.serialization.cbor.CborList /** * Common interface for CBOR parsers that can read CBOR data from different sources. diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt index 7f22ef8b2e..0374fe9b35 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt @@ -108,7 +108,7 @@ internal class CborTreeReader( } - private fun readArray(tags: ULongArray): CborList { + private fun readArray(tags: ULongArray): CborArray { val size = parser.startArray() val elements = mutableListOf() @@ -125,7 +125,7 @@ internal class CborTreeReader( parser.end() } - return CborList(elements, tags = tags) + return CborArray(elements, tags = tags) } private fun readMap(tags: ULongArray): CborMap { 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 9a59124c67..7bb0a0d126 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -46,9 +46,9 @@ internal open class CborReader(override val cbor: Cbor, protected val parser: Cb @OptIn(ExperimentalSerializationApi::class) override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { val re = if (descriptor.hasArrayTag()) { - CborListReader(cbor, parser) + CborArrayReader(cbor, parser) } else when (descriptor.kind) { - StructureKind.LIST, is PolymorphicKind -> CborListReader(cbor, parser) + StructureKind.LIST, is PolymorphicKind -> CborArrayReader(cbor, parser) StructureKind.MAP -> CborMapReader(cbor, parser) else -> CborReader(cbor, parser) } @@ -630,11 +630,11 @@ internal class StructuredCborParser(internal val element: CborElement, private v override fun startArray(tags: ULongArray?): Int { processTags(tags) - if (layer.current !is CborList) { + if (layer.current !is CborArray) { throw CborDecodingException("Expected array, got ${layer.current::class.simpleName}") } layerStack += layer - val list = layer.current as CborList + val list = layer.current as CborArray layer = PeekingIterator(list.listIterator()) return list.size //we could just return -1 and let the current layer run out of elements to never run into inconsistencies // if we do keep it like this, any inconsistencies serve as a canary for implementation bugs @@ -764,13 +764,13 @@ internal class StructuredCborParser(internal val element: CborElement, private v } -private class CborMapReader(cbor: Cbor, decoder: CborParserInterface) : CborListReader(cbor, decoder) { +private class CborMapReader(cbor: Cbor, decoder: CborParserInterface) : CborArrayReader(cbor, decoder) { override fun skipBeginToken(objectTags: ULongArray?) = setSize(parser.startMap(tags?.let { if (objectTags == null) it else ulongArrayOf(*it, *objectTags) } ?: objectTags) * 2) } -private open class CborListReader(cbor: Cbor, decoder: CborParserInterface) : CborReader(cbor, decoder) { +private open class CborArrayReader(cbor: Cbor, decoder: CborParserInterface) : CborReader(cbor, decoder) { private var ind = 0 override fun skipBeginToken(objectTags: ULongArray?) = 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 12c116bfbd..01dcc928b0 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -261,7 +261,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { } fun finalize() = when (this) { - is List -> CborList(content = elements, tags = tags) + is List -> CborArray(content = elements, tags = tags) is Map -> CborMap( content = if (elements.isNotEmpty()) { require(elements.size % 2 == 0) { diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt index 29b9006a02..fd63daa335 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt @@ -134,12 +134,12 @@ class CborElementEqualityTest { } @Test - fun testCborListEquality() { - val list1 = CborList(listOf(CborInt(1u), CborString("test"))) - val list2 = CborList(listOf(CborInt(1u), CborString("test"))) - val list3 = CborList(listOf(CborInt(2u), CborString("test"))) - val list4 = CborList(listOf(CborInt(1u), CborString("test")), 1u) - val list5 = CborList(listOf(CborInt(1u))) + fun testCborArrayEquality() { + val list1 = CborArray(listOf(CborInt(1u), CborString("test"))) + val list2 = CborArray(listOf(CborInt(1u), CborString("test"))) + val list3 = CborArray(listOf(CborInt(2u), CborString("test"))) + val list4 = CborArray(listOf(CborInt(1u), CborString("test")), 1u) + val list5 = CborArray(listOf(CborInt(1u))) assertEquals(list1, list2) assertEquals(list1.hashCode(), list2.hashCode()) @@ -210,8 +210,8 @@ class CborElementEqualityTest { @Test fun testEmptyCollectionsEquality() { - val emptyList1 = CborList(emptyList()) - val emptyList2 = CborList(emptyList()) + val emptyList1 = CborArray(emptyList()) + val emptyList2 = CborArray(emptyList()) val emptyMap1 = CborMap(emptyMap()) val emptyMap2 = CborMap(emptyMap()) @@ -227,7 +227,7 @@ class CborElementEqualityTest { fun testNestedStructureEquality() { val nested1 = CborMap( mapOf( - CborString("list") to CborList( + CborString("list") to CborArray( listOf( CborInt(1u), CborMap(mapOf(CborString("inner") to CborNull())) @@ -237,7 +237,7 @@ class CborElementEqualityTest { ) val nested2 = CborMap( mapOf( - CborString("list") to CborList( + CborString("list") to CborArray( listOf( CborInt(1u), CborMap(mapOf(CborString("inner") to CborNull())) @@ -247,7 +247,7 @@ class CborElementEqualityTest { ) val nested3 = CborMap( mapOf( - CborString("list") to CborList( + CborString("list") to CborArray( listOf( CborInt(2u), CborMap(mapOf(CborString("inner") to CborNull())) @@ -271,7 +271,7 @@ class CborElementEqualityTest { CborBoolean(true), CborByteString(byteArrayOf(1, 2, 3)), CborNull(), - CborList(listOf(CborInt(1u))), + CborArray(listOf(CborInt(1u))), CborMap(mapOf(CborString("key") to CborInt(1u))) ) @@ -291,7 +291,7 @@ class CborElementEqualityTest { CborBoolean(true) to CborBoolean(true), CborByteString(byteArrayOf(1, 2, 3)) to CborByteString(byteArrayOf(1, 2, 3)), CborNull() to CborNull(), - CborList(listOf(CborInt(1u))) to CborList(listOf(CborInt(1u))), + CborArray(listOf(CborInt(1u))) to CborArray(listOf(CborInt(1u))), CborMap(mapOf(CborString("key") to CborInt(1u))) to CborMap( mapOf( CborString("key") to CborInt( diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementSerializerBehaviorTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementSerializerBehaviorTest.kt index 7bc66524cf..1f78982cfa 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementSerializerBehaviorTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementSerializerBehaviorTest.kt @@ -71,26 +71,26 @@ class CborElementSerializerBehaviorTest { } @Test - fun typedCborListDeserializationFailsOnNonListInput() { + fun typedCborArrayDeserializationFailsOnNonListInput() { val cbor = Cbor {} val bytes = cbor.encodeToByteArray(CborElement.serializer(), CborInt(1)) assertFailsWith { - cbor.decodeFromByteArray(CborList.serializer(), bytes) + cbor.decodeFromByteArray(CborArray.serializer(), bytes) } assertFailsWith { - cbor.decodeFromCborElement(CborList.serializer(), CborInt(1)) + cbor.decodeFromCborElement(CborArray.serializer(), CborInt(1)) } } @Test fun typedCborIntDeserializationFailsOnNonIntInput() { val cbor = Cbor {} - val bytes = cbor.encodeToByteArray(CborElement.serializer(), CborList(listOf(CborInt(1)))) + val bytes = cbor.encodeToByteArray(CborElement.serializer(), CborArray(listOf(CborInt(1)))) assertFailsWith { cbor.decodeFromByteArray(CborInteger.serializer(), bytes) } assertFailsWith { - cbor.decodeFromCborElement(CborInteger.serializer(), CborList(listOf(CborInt(1)))) + cbor.decodeFromCborElement(CborInteger.serializer(), CborArray(listOf(CborInt(1)))) } } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index 56d8cd2e91..bbf97bdefc 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -49,7 +49,7 @@ class CborElementTest { fun testEncodeDecodeRootListViaCborElement() { val value = listOf(1, 2, 3) val element = cbor.encodeToCborElement(value) - assertTrue(element is CborList) + assertTrue(element is CborArray) assertEquals(value, cbor.decodeFromCborElement>(element)) } @@ -174,8 +174,8 @@ class CborElementTest { } @Test - fun testCborList() { - val listElement = CborList( + fun testCborArray() { + val listElement = CborArray( listOf( CborInt(1u), CborString("two"), @@ -187,7 +187,7 @@ class CborElementTest { val decodedList = cbor.decodeFromByteArray(listBytes) // Verify the type and size - assertTrue(decodedList is CborList) + assertTrue(decodedList is CborArray) assertEquals(4, decodedList.size) // Verify individual elements @@ -251,7 +251,7 @@ class CborElementTest { // Create a complex nested structure with maps and lists val complexElement = CborMap( mapOf( - CborString("primitives") to CborList( + CborString("primitives") to CborArray( listOf( CborInt(123u), CborString("text"), @@ -262,13 +262,13 @@ class CborElementTest { ), CborString("nested") to CborMap( mapOf( - CborString("inner") to CborList( + CborString("inner") to CborArray( listOf( CborInt(1u), CborInt(2u) ) ), - CborString("empty") to CborList(emptyList()) + CborString("empty") to CborArray(emptyList()) ) ) ) @@ -283,7 +283,7 @@ class CborElementTest { // Verify the primitives list assertTrue(decodedComplex.containsKey(CborString("primitives"))) val primitivesValue = decodedComplex[CborString("primitives")] - assertTrue(primitivesValue is CborList) + assertTrue(primitivesValue is CborArray) assertEquals(5, primitivesValue.size) @@ -311,7 +311,7 @@ class CborElementTest { // Verify the inner list assertTrue(nestedValue.containsKey(CborString("inner"))) val innerValue = nestedValue[CborString("inner")] - assertTrue(innerValue is CborList) + assertTrue(innerValue is CborArray) assertEquals(2, innerValue.size) @@ -324,7 +324,7 @@ class CborElementTest { // Verify the empty list assertTrue(nestedValue.containsKey(CborString("empty"))) val emptyValue = nestedValue[CborString("empty")] - assertTrue(emptyValue is CborList) + assertTrue(emptyValue is CborArray) val empty = emptyValue assertEquals(0, empty.size) @@ -376,8 +376,8 @@ class CborElementTest { fun testDecodeArray() { // Test data from CborParserTest.testSkipCollections val element = cbor.decodeFromHexString("830118ff1a00010000") - assertTrue(element is CborList) - val list = element as CborList + assertTrue(element is CborArray) + val list = element as CborArray assertEquals(3, list.size) assertEquals(1u, (list[0] as CborInteger).value) assertEquals(255u, (list[1] as CborInteger).value) @@ -413,7 +413,7 @@ class CborElementTest { assertEquals(CborString("Hello world"), map[CborString("b")]) // Check the array - val array = map[CborString("c")] as CborList + val array = map[CborString("c")] as CborArray assertEquals(2, array.size) assertEquals(CborString("kotlinx"), array[0]) assertEquals(CborString("serialization"), array[1]) @@ -745,8 +745,8 @@ class CborElementTest { val taggedMap = CborMap(mapOf(CborString("a") to CborInt(1)), 1uL) assertEquals(taggedMap, cbor.decodeFromByteArray(cbor.encodeToByteArray(taggedMap))) - val taggedList = CborList(listOf(CborInt(1)), 2uL) - assertEquals(taggedList, cbor.decodeFromByteArray(cbor.encodeToByteArray(taggedList))) + val taggedList = CborArray(listOf(CborInt(1)), 2uL) + assertEquals(taggedList, cbor.decodeFromByteArray(cbor.encodeToByteArray(taggedList))) val taggedFloat = CborFloat(1.5, 3uL) assertEquals(taggedFloat, cbor.decodeFromByteArray(cbor.encodeToByteArray(taggedFloat))) From 82c9e7d759ae8edea1f47444e3c8db2feb2786a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sun, 25 Jan 2026 08:54:21 +0100 Subject: [PATCH 56/68] Fix CBOR after dev rebase - Align byte parser/skip logic with dev (length checks, truncation, indefinite chunks, error messages)\n- Keep structured CBOR helpers warning-free under -Werror\n- Restore tree reader EOF semantics for CborElement decoding --- .../cbor/internal/CborElementSerializers.kt | 1 + .../cbor/internal/CborParserInterface.kt | 3 +- .../cbor/internal/CborTreeReader.kt | 26 +- .../serialization/cbor/internal/Decoder.kt | 333 ++++++++++++------ .../serialization/cbor/internal/Encoder.kt | 16 +- 5 files changed, 242 insertions(+), 137 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index 26e4b67e32..90f3d32894 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -278,6 +278,7 @@ internal fun Decoder.asCborDecoder(): CborDecoder = this as? CborDecoder ) /*need to expose writer to access encodeTag()*/ +@IgnorableReturnValue internal fun Encoder.asCborEncoder() = this as? CborEncoder ?: throw IllegalStateException( "This serializer can be used only with Cbor format. " + diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt index 970dafa99e..f1a214f566 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt @@ -39,5 +39,6 @@ internal sealed interface CborParserInterface { // Tag verification fun verifyTagsAndThrow(expected: ULongArray, actual: ULongArray?) + @IgnorableReturnValue fun processTags(tags: ULongArray?): ULongArray? -} \ No newline at end of file +} diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt index 0374fe9b35..eabea68f8b 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt @@ -55,26 +55,24 @@ internal class CborTreeReader( 7 -> { // Major type 7: simple/float/break when (parser.curByte) { 0xF4 -> { - parser.readByte() // Advance parser position - CborBoolean(false, tags = tags) + CborBoolean(parser.nextBoolean(null), tags = tags) } 0xF5 -> { - parser.readByte() // Advance parser position - CborBoolean(true, tags = tags) + CborBoolean(parser.nextBoolean(null), tags = tags) } 0xF6 -> { - parser.nextNull() + parser.nextNull(null) CborNull(tags = tags) } 0xF7 -> { - parser.readByte() // Advance parser position + parser.skipElement(null) CborUndefined(tags = tags) } // Half/Float32/Float64 - NEXT_HALF, NEXT_FLOAT, NEXT_DOUBLE -> CborFloat(parser.nextDouble(), tags = tags) + NEXT_HALF, NEXT_FLOAT, NEXT_DOUBLE -> CborFloat(parser.nextDouble(null), tags = tags) else -> throw CborDecodingException( "Invalid simple value or float type: ${parser.curByte.toString(16).uppercase()}" ) @@ -94,22 +92,18 @@ internal class CborTreeReader( * Reads any tags preceding the current value. * @return An array of tags, possibly empty */ - @OptIn(ExperimentalUnsignedTypes::class) private fun readTags(): ULongArray { - val tags = mutableListOf() + if ((parser.curByte shr 5) != 6) return EMPTY_TAGS - // Read tags (major type 6) until we encounter a non-tag + val tags = mutableListOf() while ((parser.curByte shr 5) == 6) { // Major type 6: tag - val tag = parser.nextTag() - tags.add(tag) + tags.add(parser.nextTag()) } - return tags.toULongArray() } - private fun readArray(tags: ULongArray): CborArray { - val size = parser.startArray() + val size = parser.startArray(null) val elements = mutableListOf() if (size >= 0) { @@ -129,7 +123,7 @@ internal class CborTreeReader( } private fun readMap(tags: ULongArray): CborMap { - val size = parser.startMap() + val size = parser.startMap(null) val elements = mutableMapOf() if (size >= 0) { 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 7bb0a0d126..d473709766 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -141,10 +141,19 @@ internal open class CborReader(override val cbor: Cbor, protected val parser: Cb override fun decodeBoolean() = parser.nextBoolean(tags) - override fun decodeByte() = parser.nextNumber(tags).toByte() - override fun decodeShort() = parser.nextNumber(tags).toShort() - override fun decodeChar() = parser.nextNumber(tags).toInt().toChar() - override fun decodeInt() = parser.nextNumber(tags).toInt() + private fun nextNumberWithinRange(from: Long, to: Long, type: String): Long { + val number = parser.nextNumber(tags) + if (number !in from..to) { + throw CborDecodingException("Decoded number $number is not within the range for type $type ([$from..$to])") + } + return number + } + + override fun decodeByte() = nextNumberWithinRange(Byte.MIN_VALUE.toLong(), Byte.MAX_VALUE.toLong(), "Byte").toByte() + override fun decodeShort() = nextNumberWithinRange(Short.MIN_VALUE.toLong(), Short.MAX_VALUE.toLong(), "Short").toShort() + override fun decodeChar() = + nextNumberWithinRange(Char.MIN_VALUE.code.toLong(), Char.MAX_VALUE.code.toLong(), "Char").toInt().toChar() + override fun decodeInt() = nextNumberWithinRange(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong(), "Int").toInt() override fun decodeLong() = parser.nextNumber(tags) override fun decodeNull() = parser.nextNull(tags) @@ -165,75 +174,62 @@ internal open class CborReader(override val cbor: Cbor, protected val parser: Cb internal class CborParser(private val input: ByteArrayInput, private val verifyObjectTags: Boolean) : CborParserInterface { - var curByte: Int = -1 + private var curByteOrEof: Int = -1 + + internal val curByte: Int + get() = curByteOrEof + + private fun peekCurByteOrFail(): Int { + if (curByteOrEof == -1) throw CborDecodingException("Unexpected end of encoded CBOR document") + return curByteOrEof + } init { readByte() } - fun readByte(): Int { - curByte = input.read() - return curByte + @IgnorableReturnValue + private fun readByte(): Int { + curByteOrEof = input.read() + return curByteOrEof } - fun isEof() = curByte == -1 + fun isEof() = curByteOrEof == -1 private fun skipByte(expected: Int) { - if (curByte != expected) throw CborDecodingException("byte ${printByte(expected)}", curByte) + val byte = peekCurByteOrFail() + if (byte != expected) throw CborDecodingException("byte ${printByte(expected)}", byte) readByte() } - override fun isNull() = (curByte == NULL || curByte == EMPTY_MAP || curByte == -1) - - private fun readUnsignedValueFromAdditionalInfo(additionalInfo: Int): Long { - return when (additionalInfo) { - in 0..23 -> additionalInfo.toLong() - 24 -> { - val nextByte = readByte() - if (nextByte == -1) throw CborDecodingException("Unexpected EOF") - nextByte.toLong() and 0xFF - } - - 25 -> input.readExact(2) - 26 -> input.readExact(4) - 27 -> input.readExact(8) - else -> throw CborDecodingException("Invalid additional info: $additionalInfo") - } - } - - internal fun nextTag(): ULong { - if ((curByte shr 5) != 6) { - throw CborDecodingException("Expected tag (major type 6), got major type ${curByte shr 5}") - } - - val additionalInfo = curByte and 0x1F - return readUnsignedValueFromAdditionalInfo(additionalInfo).toULong().also { skipByte(curByte) } - } + override fun isNull(): Boolean = with(peekCurByteOrFail()) { this == NULL || this == EMPTY_MAP } override fun nextNull(tags: ULongArray?): Nothing? { processTags(tags) - if (curByte == NULL) { - skipByte(NULL) - } else if (curByte == EMPTY_MAP) { - skipByte(EMPTY_MAP) + if (isNull()) { + val _ = readByte() + return null } - return null + throw CborDecodingException( + "null value (${NULL.toHexString()}) or empty map (${EMPTY_MAP.toHexString()})", + peekCurByteOrFail() + ) } override fun nextBoolean(tags: ULongArray?): Boolean { processTags(tags) - val ans = when (curByte) { + val ans = when (val byte = peekCurByteOrFail()) { TRUE -> true FALSE -> false - else -> throw CborDecodingException("boolean value", curByte) + else -> throw CborDecodingException("boolean value", byte) } readByte() return ans } - override fun startArray(tags: ULongArray?) = startSized(tags, BEGIN_ARRAY, HEADER_ARRAY, "array") + override fun startArray(tags: ULongArray?): Int = startSized(tags, BEGIN_ARRAY, HEADER_ARRAY, "array") - override fun startMap(tags: ULongArray?) = startSized(tags, BEGIN_MAP, HEADER_MAP, "map") + override fun startMap(tags: ULongArray?): Int = startSized(tags, BEGIN_MAP, HEADER_MAP, "map") private fun startSized( tags: ULongArray?, @@ -242,57 +238,88 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO collectionType: String ): Int { processTags(tags) - if (curByte == unboundedHeader) { + val header = peekCurByteOrFail() + if (header == unboundedHeader) { skipByte(unboundedHeader) return -1 } - if ((curByte and 0b111_00000) != boundedHeaderMask) - throw CborDecodingException("start of $collectionType", curByte) - val size = readNumber().toInt() + if ((header and MAJOR_TYPE_MASK) != boundedHeaderMask) { + if (boundedHeaderMask == HEADER_ARRAY && (header and MAJOR_TYPE_MASK) == HEADER_BYTE_STRING) { + throw CborDecodingException( + "Expected a start of array, " + + "but found ${printByte(header)}, which corresponds to the start of a byte string. " + + "Make sure you correctly set 'alwaysUseByteString' setting " + + "and/or 'kotlinx.serialization.cbor.ByteString' annotation." + ) + } + throw CborDecodingException("start of $collectionType", header) + } + val majorType = header and MAJOR_TYPE_MASK + val sizeLimit = if (majorType == HEADER_MAP) Int.MAX_VALUE / 2 else Int.MAX_VALUE + val size = readUnsignedIntegerIgnoringMajorType { "$collectionType length" } + .asSizedElementLength(majorType, sizeLimit) readByte() return size } - override fun isEnd() = curByte == BREAK + override fun isEnd(): Boolean = peekCurByteOrFail() == BREAK - override fun end() = skipByte(BREAK) + override fun end() { + skipByte(BREAK) + } override fun nextByteString(tags: ULongArray?): ByteArray { processTags(tags) - if ((curByte and 0b111_00000) != HEADER_BYTE_STRING) - throw CborDecodingException("start of byte string", curByte) + val header = peekCurByteOrFail() + if ((header and MAJOR_TYPE_MASK) != HEADER_BYTE_STRING) { + if (header and MAJOR_TYPE_MASK == HEADER_ARRAY) { + throw CborDecodingException( + "Expected a start of a byte string, " + + "but found ${printByte(header)}, which corresponds to the start of an array. " + + "Make sure you correctly set 'alwaysUseByteString' setting " + + "and/or 'kotlinx.serialization.cbor.ByteString' annotation." + ) + } + throw CborDecodingException("start of byte string", header) + } val arr = readBytes() readByte() return arr } - override fun nextString(tags: ULongArray?) = nextTaggedString(tags).first + override fun nextString(tags: ULongArray?): String = nextTaggedString(tags).first //used for reading the tag names and names of tagged keys (of maps, and serialized classes) private fun nextTaggedString(tags: ULongArray?): Pair { val collectedTags = processTags(tags) - if ((curByte and 0b111_00000) != HEADER_STRING) - throw CborDecodingException("start of string", curByte) + val headerByte = peekCurByteOrFail() + if ((headerByte and MAJOR_TYPE_MASK) != HEADER_STRING) + throw CborDecodingException("start of string", headerByte) val arr = readBytes() val ans = arr.decodeToString() readByte() return ans to collectedTags } - private fun readBytes(): ByteArray = - if (curByte and 0b000_11111 == ADDITIONAL_INFORMATION_INDEFINITE_LENGTH) { + private fun readBytes(): ByteArray { + val headerByte = peekCurByteOrFail() + return if (headerByte and ADDITIONAL_INFO_MASK == ADDITIONAL_INFORMATION_INDEFINITE_LENGTH) { + val majorType = headerByte and MAJOR_TYPE_MASK readByte() - readIndefiniteLengthBytes() + readIndefiniteLengthStringChunks(majorType) } else { - val strLen = readNumber().toInt() + val majorType = headerByte and MAJOR_TYPE_MASK + val strLen = readUnsignedIntegerIgnoringMajorType { "length" }.asSizedElementLength(majorType) input.readExactNBytes(strLen) } + } + @IgnorableReturnValue override fun processTags(tags: ULongArray?): ULongArray? { var index = 0 val collectedTags = mutableListOf() - while ((curByte and 0b111_00000) == HEADER_TAG) { - val readTag = readNumber().toULong() // This is the tag number + while ((peekCurByteOrFail() and MAJOR_TYPE_MASK) == HEADER_TAG) { + val readTag = readUnsignedIntegerIgnoringMajorType { "tag" }.toULong() // This is the tag number collectedTags += readTag // value tags and object tags are intermingled (keyTags are always separate) // so this check only holds if we verify both @@ -312,9 +339,9 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO // If we don't care for object tags, the best we can do is assure that the collected tags start with // the expected tags. (yes this could co somewhere else, but putting it here groups the code nicely // into if-else branches. - if ((collectedTags.size < it.size) - || (collectedTags.subList(0, it.size) != it.asList()) - ) throw CborDecodingException("CBOR tags $collectedTags do not start with specified tags $it") + if ((collectedTags.size < it.size) || (collectedTags.subList(0, it.size) != it.asList())) { + throw CborDecodingException("CBOR tags $collectedTags do not start with specified tags $it") + } } } } @@ -332,19 +359,19 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO */ override fun nextTaggedStringOrNumber(): Triple { val collectedTags = processTags(null) - if ((curByte and 0b111_00000) == HEADER_STRING) { + val majorType = peekCurByteOrFail() + return if ((majorType and MAJOR_TYPE_MASK) == HEADER_STRING) { val arr = readBytes() val ans = arr.decodeToString() readByte() - return Triple(ans, null, collectedTags) + Triple(ans, null, collectedTags) } else { - val res = readNumber() + val res = readUnsignedIntegerIgnoringMajorType { majorType.majorTypeName } readByte() - return Triple(null, res, collectedTags) + Triple(null, res, collectedTags) } } - override fun nextNumber(tags: ULongArray?): Long { processTags(tags) val res = readNumber() @@ -352,22 +379,34 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO return res } - internal fun nextULong(tags: ULongArray? = null): ULong { - processTags(tags) - val res = readNumber(signed = false) - readByte() - return res.toULong() + // Reads a value encoded using rules for the major type 0 (a.k.a. unsigned integers) + private inline fun readUnsignedIntegerIgnoringMajorType(valueDescriptionForError: () -> String): Long { + val additionalInfo = peekCurByteOrFail() and ADDITIONAL_INFO_MASK + + if (additionalInfo <= 23) return additionalInfo.toLong() + val bytesToRead = when (additionalInfo) { + 24 -> 1 + 25 -> 2 + 26 -> 4 + 27 -> 8 + else -> throw CborDecodingException( + "Unexpected value encoding when reading ${valueDescriptionForError()}. " + + "Expected addition info value < 28, got $additionalInfo " + + "(decoded from ${printByte(peekCurByteOrFail())})" + ) + } + return input.readExact(bytesToRead) } - private fun readNumber(signed: Boolean = true): Long { - val additionalInfo = curByte and 0b000_11111 - val negative = (curByte and 0b111_00000) == HEADER_NEGATIVE.toInt() - - val value = readUnsignedValueFromAdditionalInfo(additionalInfo) - - return if (signed) { - if (negative) -(value + 1) else value - } else value + private fun readNumber(): Long { + val headerByte = peekCurByteOrFail() + val majorType = headerByte and MAJOR_TYPE_MASK + if (majorType != HEADER_NEGATIVE.toInt() && majorType != HEADER_POSITIVE.toInt()) { + throw CborDecodingException("an unsigned or negative integer", headerByte) + } + val negative = majorType == HEADER_NEGATIVE.toInt() + val unsignedValue = readUnsignedIntegerIgnoringMajorType { majorType.majorTypeName } + return if (negative) -(unsignedValue + 1) else unsignedValue } private fun ByteArrayInput.readExact(bytes: Int): Long { @@ -379,21 +418,25 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO return result } - private fun ByteArrayInput.readExactNBytes(bytesCount: Int): ByteArray { + private fun ByteArrayInput.ensureEnoughBytes(bytesCount: Int) { if (bytesCount > availableBytes) { - error("Unexpected EOF, available $availableBytes bytes, requested: $bytesCount") + throw CborDecodingException("Unexpected EOF, available $availableBytes bytes, requested: $bytesCount") } + } + + private fun ByteArrayInput.readExactNBytes(bytesCount: Int): ByteArray { + ensureEnoughBytes(bytesCount) val array = ByteArray(bytesCount) - read(array, 0, bytesCount) + val _ = read(array, 0, bytesCount) return array } override fun nextFloat(tags: ULongArray?): Float { processTags(tags) - val res = when (curByte) { + val res = when (val headerByte = peekCurByteOrFail()) { NEXT_FLOAT -> Float.fromBits(readInt()) NEXT_HALF -> floatFromHalfBits(readShort()) - else -> throw CborDecodingException("float header", curByte) + else -> throw CborDecodingException("float header", headerByte) } readByte() return res @@ -401,19 +444,20 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO override fun nextDouble(tags: ULongArray?): Double { processTags(tags) - val res = when (curByte) { + val res = when (val headerByte = peekCurByteOrFail()) { NEXT_DOUBLE -> Double.fromBits(readLong()) NEXT_FLOAT -> Float.fromBits(readInt()).toDouble() NEXT_HALF -> floatFromHalfBits(readShort()).toDouble() - else -> throw CborDecodingException("double header", curByte) + else -> throw CborDecodingException("double header", headerByte) } readByte() return res } private fun readLong(): Long { + input.ensureEnoughBytes(Long.SIZE_BYTES) var result = 0L - for (i in 0..7) { + repeat(Long.SIZE_BYTES) { val byte = input.read() result = (result shl 8) or byte.toLong() } @@ -421,14 +465,16 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO } private fun readShort(): Short { + input.ensureEnoughBytes(Short.SIZE_BYTES) val highByte = input.read() val lowByte = input.read() return (highByte shl 8 or lowByte).toShort() } private fun readInt(): Int { + input.ensureEnoughBytes(Int.SIZE_BYTES) var result = 0 - for (i in 0..3) { + repeat(Int.SIZE_BYTES) { val byte = input.read() result = (result shl 8) or byte } @@ -450,6 +496,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO override fun skipElement(tags: ULongArray?) { val lengthStack = mutableListOf() + if (isEof()) throw CborDecodingException("Unexpected EOF while skipping element") processTags(tags) do { @@ -459,13 +506,13 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO lengthStack.add(LENGTH_STACK_INDEFINITE) } else if (isEnd()) { if (lengthStack.removeLastOrNull() != LENGTH_STACK_INDEFINITE) - throw CborDecodingException("next data item", curByte) + throw CborDecodingException("next data item", peekCurByteOrFail()) prune(lengthStack) } else { - val header = curByte and 0b111_00000 + val header = peekCurByteOrFail() and MAJOR_TYPE_MASK val length = elementLength() if (header == HEADER_TAG) { - readNumber() + val _ = readUnsignedIntegerIgnoringMajorType { "tag" } } else if (header == HEADER_ARRAY || header == HEADER_MAP) { if (length > 0) lengthStack.add(length) else prune(lengthStack) // empty map or array automatically completes @@ -499,14 +546,15 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO } /** - * Determines if [curByte] represents an indefinite length CBOR item. + * Determines if [peekCurByteOrFail] represents an indefinite length CBOR item. * - * Per [RFC 7049: 2.2. Indefinite Lengths for Some Major Types](https://tools.ietf.org/html/rfc7049#section-2.2): + * Per [RFC 8949: 3.2. Indefinite Lengths for Some Major Types](https://tools.ietf.org/html/rfc8949#section-3.2): * > Four CBOR items (arrays, maps, byte strings, and text strings) can be encoded with an indefinite length */ private fun isIndefinite(): Boolean { - val majorType = curByte and 0b111_00000 - val value = curByte and 0b000_11111 + val curByte = peekCurByteOrFail() + val majorType = curByte and MAJOR_TYPE_MASK + val value = curByte and ADDITIONAL_INFO_MASK return value == ADDITIONAL_INFORMATION_INDEFINITE_LENGTH && (majorType == HEADER_ARRAY || majorType == HEADER_MAP || @@ -514,7 +562,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO } /** - * Determines the length of the CBOR item represented by [curByte]; length has specific meaning based on the type: + * Determines the length of the CBOR item represented by [peekCurByteOrFail]; length has specific meaning based on the type: * * | Major type | Length represents number of... | * |---------------------|--------------------------------| @@ -527,37 +575,93 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO * | 6. tag | bytes | */ private fun elementLength(): Int { - val majorType = curByte and 0b111_00000 - val additionalInformation = curByte and 0b000_11111 + val curByte = peekCurByteOrFail() + val majorType = curByte and MAJOR_TYPE_MASK + val additionalInformation = curByte and ADDITIONAL_INFO_MASK return when (majorType) { - HEADER_BYTE_STRING, HEADER_STRING, HEADER_ARRAY -> readNumber().toInt() - HEADER_MAP -> readNumber().toInt() * 2 + HEADER_BYTE_STRING, HEADER_STRING, HEADER_ARRAY -> + readUnsignedIntegerIgnoringMajorType { "${majorType.majorTypeName} length" }.asSizedElementLength(majorType) + HEADER_MAP -> + readUnsignedIntegerIgnoringMajorType { "map length" }.asSizedElementLength(majorType, Int.MAX_VALUE / 2) * 2 else -> when (additionalInformation) { 24 -> 1 25 -> 2 26 -> 4 27 -> 8 else -> 0 - } + } } } /** - * Indefinite-length byte sequences contain an unknown number of fixed-length byte sequences (chunks). + * Reads fixed-length chunks constituting indefinite-length byte sequences (either a text, or a byte-string). * + * @param majorType a type of the enclosing indefinite-length sequence ([HEADER_STRING] or [HEADER_BYTE_STRING]) * @return [ByteArray] containing all of the concatenated bytes found in the buffer. */ - private fun readIndefiniteLengthBytes(): ByteArray { + private fun readIndefiniteLengthStringChunks(majorType: Int): ByteArray { val byteStrings = mutableListOf() do { - byteStrings.add(readBytes()) + val header = peekCurByteOrFail() + if (header and MAJOR_TYPE_MASK != majorType) { + throw CborDecodingException( + "a header of a chunk with a major type bits matching $majorType", + header + ) + } + if (header and ADDITIONAL_INFO_MASK == ADDITIONAL_INFORMATION_INDEFINITE_LENGTH) { + throw CborDecodingException("a fixed-length chunk", header) + } + val length = readUnsignedIntegerIgnoringMajorType { "length of a fixed-length chunk" } + .asSizedElementLength(majorType) + byteStrings.add(input.readExactNBytes(length)) readByte() } while (!isEnd()) return byteStrings.flatten() } + + private fun Long.asSizedElementLength(majorType: Int, sizeLimit: Int = Int.MAX_VALUE): Int { + if (this in 0L..sizeLimit.toLong()) return this.toInt() + + val typeName = majorType.majorTypeName + + if (this < 0) { + throw CborDecodingException("negative length value was decoded for $typeName: $this") + } + throw CborDecodingException("length for $typeName is too large: $this") + } + + internal fun nextULong(tags: ULongArray? = null): ULong { + processTags(tags) + val res = readUnsignedIntegerIgnoringMajorType { "unsigned integer" } + readByte() + return res.toULong() + } + + internal fun nextTag(): ULong { + val header = peekCurByteOrFail() + if ((header and MAJOR_TYPE_MASK) != HEADER_TAG) { + throw CborDecodingException("start of tag", header) + } + val tag = readUnsignedIntegerIgnoringMajorType { "tag" }.toULong() + readByte() + return tag + } } +private val Int.majorTypeName: String + get() = when (this and MAJOR_TYPE_MASK) { + HEADER_BYTE_STRING -> "byte string" + HEADER_STRING -> "string" + HEADER_ARRAY -> "array" + HEADER_MAP -> "map" + HEADER_TAG -> "tag" + HEADER_POSITIVE.toInt() -> "unsigned integer" + HEADER_NEGATIVE.toInt() -> "negative integer" + else -> "" + } + private fun Iterable.flatten(): ByteArray { val output = ByteArray(sumOf { it.size }) var position = 0 @@ -586,13 +690,13 @@ internal class PeekingIterator private constructor( fun peek() = if (hasNext()) { val next = iter.next() - iter.previous() + val _ = iter.previous() next } else null companion object { operator fun invoke(single: CborElement): PeekingIterator = - PeekingIterator(false, listOf(single).listIterator()).also { it.next() } + PeekingIterator(false, listOf(single).listIterator()).also { val _ = it.next() } operator fun invoke(iter: ListIterator): PeekingIterator = PeekingIterator(true, iter) @@ -730,10 +834,13 @@ internal class StructuredCborParser(internal val element: CborElement, private v * The reason this method mixes two behaviours is that decoding a primitive is invoked on a single element. * `decodeElementIndex`, etc. is invoked on an iterable and there are key tags and value tags */ + @IgnorableReturnValue override fun processTags(tags: ULongArray?): ULongArray? { // If we're in a list/map, advance to the next element - if (layer.hasNext()) layer.next() + if (layer.hasNext()) { + val _ = layer.next() + } // if we're at a primitive, we only process tags // Store collected tags for verification @@ -789,7 +896,7 @@ private val normalizeBaseBits = SINGLE_PRECISION_NORMALIZE_BASE.toBits() /* - * For details about half-precision floating-point numbers see https://tools.ietf.org/html/rfc7049#appendix-D + * For details about half-precision floating-point numbers see https://tools.ietf.org/html/rfc8949#name-half-precision */ private fun floatFromHalfBits(bits: Short): Float { val intBits = bits.toInt() 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 01dcc928b0..5ca654b641 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -246,7 +246,9 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { */ internal sealed class CborContainer(private val tags: ULongArray, protected val elements: MutableList) { - open fun add(element: CborElement) = elements.add(element) + open fun add(element: CborElement) { + elements.add(element) + } class Map(tags: ULongArray, elements: MutableList = mutableListOf()) : CborContainer(tags, elements) @@ -254,9 +256,9 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { CborContainer(tags, elements) class Root(tags: ULongArray) : CborContainer(tags, elements = mutableListOf()) { - override fun add(element: CborElement): Boolean { + override fun add(element: CborElement) { require(elements.isEmpty()) { "Implementation error. Please report a bug." } - return elements.add(element) + elements.add(element) } } @@ -283,8 +285,8 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { } } - private operator fun CborContainer?.plusAssign(element: CborElement) { - this!!.add(element) + private operator fun CborContainer.plusAssign(element: CborElement) { + add(element) } private val stack = ArrayDeque() @@ -313,14 +315,14 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { else -> CborContainer.Map(tags) } } - currentElement?.let { stack.add(it) } + stack.addLast(currentElement) currentElement = element return this } override fun endStructure(descriptor: SerialDescriptor) { ensureNoDanglingTags("endStructure") - val finalized = currentElement!!.finalize() + val finalized = currentElement.finalize() if (stack.isNotEmpty()) { currentElement = stack.removeLast() currentElement += finalized From 8a881dca6ee3c02da33fc6a0559017e03ed74544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sun, 25 Jan 2026 09:28:58 +0100 Subject: [PATCH 57/68] Remove copyright headers from new CBOR files --- .../src/kotlinx/serialization/cbor/CborElement.kt | 4 ---- .../serialization/cbor/internal/CborElementSerializers.kt | 3 --- .../serialization/cbor/internal/CborParserInterface.kt | 3 --- .../kotlinx/serialization/cbor/internal/CborTreeReader.kt | 3 --- .../kotlinx/serialization/cbor/CborElementEqualityTest.kt | 6 +----- .../src/kotlinx/serialization/cbor/CborElementTest.kt | 4 ---- 6 files changed, 1 insertion(+), 22 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt index ee54dfb146..0984d0828c 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -1,7 +1,3 @@ -/* - * Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - @file:Suppress("unused") @file:OptIn(ExperimentalUnsignedTypes::class) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index 90f3d32894..d161b996b0 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -1,6 +1,3 @@ -/* - * Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ @file:OptIn(ExperimentalSerializationApi::class, ExperimentalUnsignedTypes::class) package kotlinx.serialization.cbor.internal diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt index f1a214f566..c40bbd1ab4 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt @@ -1,6 +1,3 @@ -/* - * Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ @file:OptIn(ExperimentalSerializationApi::class, ExperimentalUnsignedTypes::class) package kotlinx.serialization.cbor.internal diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt index eabea68f8b..afc7381f91 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt @@ -1,6 +1,3 @@ -/* - * Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ @file:OptIn(ExperimentalSerializationApi::class, ExperimentalUnsignedTypes::class) package kotlinx.serialization.cbor.internal diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt index fd63daa335..4af064e617 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt @@ -1,7 +1,3 @@ -/* - * Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - package kotlinx.serialization.cbor import kotlin.test.* @@ -318,4 +314,4 @@ class CborElementEqualityTest { assertEquals(b, c) assertEquals(a, c, "Transitivity: if a==b and b==c, then a==c") } -} \ No newline at end of file +} diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index bbf97bdefc..785e2bd510 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -1,7 +1,3 @@ -/* - * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - package kotlinx.serialization.cbor import kotlinx.serialization.* From 4b88575c959d38fddeb9d8938eb092a79d43356d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sun, 25 Jan 2026 10:01:41 +0100 Subject: [PATCH 58/68] Add test documenting unsigned CBOR interop issue --- .../cbor/CborUnsignedInteroperabilityTest.kt | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborUnsignedInteroperabilityTest.kt diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborUnsignedInteroperabilityTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborUnsignedInteroperabilityTest.kt new file mode 100644 index 0000000000..f56ed75f5a --- /dev/null +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborUnsignedInteroperabilityTest.kt @@ -0,0 +1,37 @@ +@file:OptIn(ExperimentalUnsignedTypes::class, ExperimentalSerializationApi::class) + +package kotlinx.serialization.cbor + +import kotlinx.serialization.* +import kotlin.test.* + +class CborUnsignedInteroperabilityTest { + + @Test + fun testUByteIsNotInteroperableWithCborUnsignedIntegerEncoding() { + // Canonical CBOR encoding for unsigned integer 200 is 0x18 0xC8. + val canonicalUnsigned200 = "18c8" + + val element = Cbor.decodeFromHexString(canonicalUnsigned200) + val asInteger = element as? CborInteger ?: fail("Expected CborInteger, got ${element::class}") + assertTrue(asInteger.isPositive) + assertEquals(200uL, asInteger.value) + + // Kotlin UByte serializer is implemented via Byte serializer (two's-complement mapping), + // so decoding a canonical unsigned integer > 127 fails due to Byte range checks. + assertEquals(200.toUByte(), Cbor.decodeFromHexString(canonicalUnsigned200)) + + + // Encoding UByte(200) does not produce the canonical unsigned integer representation. + val encodedByCurrentSerializer = Cbor.encodeToHexString(200u.toUByte()) + assertEquals(canonicalUnsigned200, encodedByCurrentSerializer) + + // It round-trips within the library, but the CBOR representation is a negative integer. + val encodedElement = Cbor.decodeFromHexString(encodedByCurrentSerializer) + val encodedAsInteger = + encodedElement as? CborInteger ?: fail("Expected CborInteger, got ${encodedElement::class}") + //is actually -56, not 200 + assertEquals(200L, encodedAsInteger.long) + } +} + From 4ccf30b1c2365ac986b0cf3a963b6f9efb2c4afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sun, 25 Jan 2026 10:23:09 +0100 Subject: [PATCH 59/68] CBOR: encode/decode Kotlin unsigned types as CBOR unsigned ints Handle kotlin.UByte/UShort/UInt/ULong inline serializers via format-specific encodeInline/decodeInline wrappers so values use CBOR major type 0 and encode/decode correctly. --- .../serialization/cbor/internal/Decoder.kt | 16 +++- .../serialization/cbor/internal/Encoder.kt | 12 ++- .../serialization/cbor/internal/Unsigned.kt | 88 +++++++++++++++++++ .../cbor/CborUnsignedInteroperabilityTest.kt | 38 +++++--- 4 files changed, 137 insertions(+), 17 deletions(-) create mode 100644 formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Unsigned.kt 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 d473709766..e75a77ffea 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -12,7 +12,7 @@ import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* import kotlinx.serialization.modules.* -internal open class CborReader(override val cbor: Cbor, protected val parser: CborParserInterface) : AbstractDecoder(), +internal open class CborReader(override val cbor: Cbor, internal val parser: CborParserInterface) : AbstractDecoder(), CborDecoder { override fun decodeCborElement(): CborElement = @@ -29,7 +29,7 @@ internal open class CborReader(override val cbor: Cbor, protected val parser: Cb private var readProperties: Int = 0 protected var decodeByteArrayAsByteString = false - protected var tags: ULongArray? = null + internal var tags: ULongArray? = null protected fun setSize(size: Int) { if (size >= 0) { @@ -41,6 +41,17 @@ internal open class CborReader(override val cbor: Cbor, protected val parser: Cb override val serializersModule: SerializersModule get() = cbor.serializersModule + override fun decodeInline(descriptor: SerialDescriptor): Decoder { + return when (descriptor.serialName) { + "kotlin.UByte", + "kotlin.UShort", + "kotlin.UInt", + "kotlin.ULong", + -> UnsignedInlineDecoder(this) + else -> super.decodeInline(descriptor) + } + } + protected open fun skipBeginToken(objectTags: ULongArray?) = setSize(parser.startMap(objectTags)) @OptIn(ExperimentalSerializationApi::class) @@ -170,6 +181,7 @@ internal open class CborReader(override val cbor: Cbor, protected val parser: Cb } } } + } internal class CborParser(private val input: ByteArrayInput, private val verifyObjectTags: Boolean) : 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 5ca654b641..447fb9aa6a 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -91,6 +91,17 @@ internal sealed class CborWriter( override fun shouldEncodeElementDefault(descriptor: SerialDescriptor, index: Int): Boolean = cbor.configuration.encodeDefaults + override fun encodeInline(descriptor: SerialDescriptor): Encoder { + return when (descriptor.serialName) { + "kotlin.UByte", + "kotlin.UShort", + "kotlin.UInt", + "kotlin.ULong", + -> UnsignedInlineEncoder(this) + else -> super.encodeInline(descriptor) + } + } + protected abstract fun incrementChildren() override fun encodeString(value: String) { @@ -199,7 +210,6 @@ internal sealed class CborWriter( } } - // optimized indefinite length encoder internal class IndefiniteLengthCborWriter(cbor: Cbor, private val output: ByteArrayOutput) : CborWriter( cbor diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Unsigned.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Unsigned.kt new file mode 100644 index 0000000000..210148ea24 --- /dev/null +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Unsigned.kt @@ -0,0 +1,88 @@ +@file:OptIn(ExperimentalSerializationApi::class, ExperimentalUnsignedTypes::class) + +package kotlinx.serialization.cbor.internal + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.cbor.CborInteger +import kotlinx.serialization.cbor.longOrNull +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +internal class UnsignedInlineEncoder( + private val delegate: CborWriter, +) : Encoder by delegate { + override fun encodeByte(value: Byte) { + delegate.encodePositive(value.toUByte().toULong()) + } + + override fun encodeShort(value: Short) { + delegate.encodePositive(value.toUShort().toULong()) + } + + override fun encodeInt(value: Int) { + delegate.encodePositive(value.toUInt().toULong()) + } + + override fun encodeLong(value: Long) { + delegate.encodePositive(value.toULong()) + } +} + +internal class UnsignedInlineDecoder( + private val delegate: CborReader, +) : Decoder by delegate { + + override fun decodeByte(): Byte { + val value = delegate.parser.nextNumber(delegate.tags) + if (value !in 0L..UByte.MAX_VALUE.toLong()) { + throw CborDecodingException("Decoded number $value is not within the range for type UByte ([0..255])") + } + return value.toByte() + } + + override fun decodeShort(): Short { + val value = delegate.parser.nextNumber(delegate.tags) + if (value !in 0L..UShort.MAX_VALUE.toLong()) { + throw CborDecodingException("Decoded number $value is not within the range for type UShort ([0..65535])") + } + return value.toShort() + } + + override fun decodeInt(): Int { + val value = delegate.parser.nextNumber(delegate.tags) + if (value !in 0L..UInt.MAX_VALUE.toLong()) { + throw CborDecodingException( + "Decoded number $value is not within the range for type UInt ([0..${UInt.MAX_VALUE.toLong()}])" + ) + } + return value.toInt() + } + + override fun decodeLong(): Long { + val parser = delegate.parser + val tags = delegate.tags + + return when (parser) { + is StructuredCborParser -> { + parser.processTags(tags) + val element = parser.layer.current + val integer = element as? CborInteger + ?: throw CborDecodingException("Expected number, got ${element::class.simpleName}") + + if (!integer.isPositive) { + throw CborDecodingException("Expected unsigned integer, got $integer") + } + integer.value.toLong() + } + + is CborParser -> { + parser.processTags(tags) + val header = parser.curByte + if ((header and MAJOR_TYPE_MASK) != HEADER_POSITIVE.toInt()) { + throw CborDecodingException("unsigned integer", header) + } + parser.nextULong(null).toLong() + } + } + } +} diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborUnsignedInteroperabilityTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborUnsignedInteroperabilityTest.kt index f56ed75f5a..486f81e156 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborUnsignedInteroperabilityTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborUnsignedInteroperabilityTest.kt @@ -8,7 +8,7 @@ import kotlin.test.* class CborUnsignedInteroperabilityTest { @Test - fun testUByteIsNotInteroperableWithCborUnsignedIntegerEncoding() { + fun testUnsignedInlineTypesUseCborUnsignedIntegerEncoding() { // Canonical CBOR encoding for unsigned integer 200 is 0x18 0xC8. val canonicalUnsigned200 = "18c8" @@ -17,21 +17,31 @@ class CborUnsignedInteroperabilityTest { assertTrue(asInteger.isPositive) assertEquals(200uL, asInteger.value) - // Kotlin UByte serializer is implemented via Byte serializer (two's-complement mapping), - // so decoding a canonical unsigned integer > 127 fails due to Byte range checks. - assertEquals(200.toUByte(), Cbor.decodeFromHexString(canonicalUnsigned200)) + assertEquals(200u.toUByte(), Cbor.decodeFromHexString(canonicalUnsigned200)) + assertEquals(canonicalUnsigned200, Cbor.encodeToHexString(200u.toUByte())) + val canonicalUInt = "1aee6b2800" // 4_000_000_000u + assertEquals(4_000_000_000u, Cbor.decodeFromHexString(canonicalUInt)) + assertEquals(canonicalUInt, Cbor.encodeToHexString(4_000_000_000u)) - // Encoding UByte(200) does not produce the canonical unsigned integer representation. - val encodedByCurrentSerializer = Cbor.encodeToHexString(200u.toUByte()) - assertEquals(canonicalUnsigned200, encodedByCurrentSerializer) + val canonicalULongMax = "1bffffffffffffffff" + assertEquals(ULong.MAX_VALUE, Cbor.decodeFromHexString(canonicalULongMax)) + assertEquals(canonicalULongMax, Cbor.encodeToHexString(ULong.MAX_VALUE)) - // It round-trips within the library, but the CBOR representation is a negative integer. - val encodedElement = Cbor.decodeFromHexString(encodedByCurrentSerializer) - val encodedAsInteger = - encodedElement as? CborInteger ?: fail("Expected CborInteger, got ${encodedElement::class}") - //is actually -56, not 200 - assertEquals(200L, encodedAsInteger.long) + // Structured decoding from CborElement also supports unsigned values beyond Long range. + assertEquals(ULong.MAX_VALUE, Cbor.decodeFromCborElement(CborInt(ULong.MAX_VALUE))) } -} + @Test + fun testLegacySignedEncodingsAreRejectedForUnsignedTypes() { + // Legacy (incorrect) encodings produced by previous versions, which encoded unsigned Kotlin value classes + // via signed primitives (e.g. UByteSerializer -> encodeByte(value.toByte())). + assertFailsWith { Cbor.decodeFromHexString("3837") } // -56 -> 200u + assertFailsWith { Cbor.decodeFromHexString("3963bf") } // -25536 -> 40000u + assertFailsWith { Cbor.decodeFromHexString("3a1194d7ff") } // -294967296 -> 4_000_000_000u + assertFailsWith { Cbor.decodeFromHexString("20") } // -1 -> ULong.MAX_VALUE + + // Structured decoding should also reject negative integers for unsigned Kotlin types. + assertFailsWith { Cbor.decodeFromCborElement(CborInt(-1)) } + } +} From 8977d2a5cddea52812a8aab283e0c93fff9805a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sun, 25 Jan 2026 11:40:00 +0100 Subject: [PATCH 60/68] CBOR: add map-backed delegated properties test --- .../cbor/CborDelegatedPropertiesTest.kt | 258 ++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDelegatedPropertiesTest.kt diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDelegatedPropertiesTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDelegatedPropertiesTest.kt new file mode 100644 index 0000000000..5e8f4f0a1e --- /dev/null +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDelegatedPropertiesTest.kt @@ -0,0 +1,258 @@ +/* + * Part of the kotlinx.serialization test suite. + * + * Copyright 2025 Bernd Prünster (A-SIT Plus GmbH). + * Use of this source code is governed by the Apache 2.0 license. + */ + +@file:OptIn(kotlinx.serialization.ExperimentalSerializationApi::class, kotlin.ExperimentalUnsignedTypes::class) + +package kotlinx.serialization.cbor + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class CborDelegatedPropertiesTest { + + private companion object { + private const val NAME_LABEL: Long = 1 + private const val AGE_LABEL: Long = 2 + private const val GENDER_LABEL: Long = 3 + + private val NAME_SPEC = FieldSpec(name = "name", label = NAME_LABEL) + private val AGE_SPEC = FieldSpec(name = "age", label = AGE_LABEL) + private val GENDER_SPEC = FieldSpec(name = "gender", label = GENDER_LABEL) + } + + /** + * Showcases Kotlin delegated properties backed by a map: + * https://kotlinlang.org/docs/delegated-properties.html#storing-properties-in-a-map + * + * kotlinx.serialization does not support such delegated properties out of the box, so this test provides + * a CBOR-specific serializer that encodes/decodes the underlying map. + */ + @Serializable(with = MapBackedPersonSerializer::class) + private class MapBackedPerson private constructor( + private val content: MutableMap, + public val backing: CborMap = CborMap(content), + ) { + + var name: String by RequiredCborString(content, NAME_SPEC) + var age: Int by RequiredCborInt(content, AGE_SPEC) + + constructor(name: String, age: Int) : this(content = mutableMapOf()) { + this.name = name + this.age = age + } + + fun put( + key: String, + value: CborElement, + keyTags: ULongArray = ulongArrayOf(), + valueTags: ULongArray = ulongArrayOf(), + ) { + content[CborString(key, *keyTags)] = value.also { it.tags += valueTags } + } + + fun remove(key: String, keyTags: ULongArray = ulongArrayOf()) { + content.remove(CborString(key, *keyTags)) + } + + companion object { + fun fromBacking(map: CborMap): MapBackedPerson { + val content = mutableMapOf() + for ((key, value) in map.entries) { + val mappedKey = when (key) { + is CborInteger -> when { + key.isPositive && key.value == NAME_LABEL.toULong() -> CborString(NAME_SPEC.name, *NAME_SPEC.keyTags) + key.isPositive && key.value == AGE_LABEL.toULong() -> CborString(AGE_SPEC.name, *AGE_SPEC.keyTags) + key.isPositive && key.value == GENDER_LABEL.toULong() -> CborString(GENDER_SPEC.name, *GENDER_SPEC.keyTags) + else -> key + } + + else -> key + } + content[mappedKey] = value + } + return MapBackedPerson(content, backing = CborMap(content, *map.tags)) + } + } + + override fun equals(other: Any?): Boolean = + other is MapBackedPerson && backing == other.backing + + override fun hashCode(): Int = backing.hashCode() + + override fun toString(): String = "MapBackedPerson(backing=$backing)" + } + + private class RequiredCborString( + private val content: MutableMap, + private val spec: FieldSpec, + ) : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): String { + val element = content[CborString(spec.name, *spec.keyTags)] + ?: throw SerializationException("Missing required '${spec.name}' property") + return (element as? CborString)?.value + ?: throw SerializationException("Expected '${spec.name}' to be CborString, got ${element::class}") + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { + content[CborString(spec.name, *spec.keyTags)] = CborString(value, *spec.valueTags) + } + } + + private class RequiredCborInt( + private val content: MutableMap, + private val spec: FieldSpec, + ) : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): Int { + val element = content[CborString(spec.name, *spec.keyTags)] + ?: throw SerializationException("Missing required '${spec.name}' property") + val integer = element as? CborInteger + ?: throw SerializationException("Expected '${spec.name}' to be CborInteger, got ${element::class}") + return integer.intOrNull + ?: throw SerializationException("Expected '${spec.name}' to fit into Int, got $integer") + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) { + content[CborString(spec.name, *spec.keyTags)] = CborInt(value.toLong(), *spec.valueTags) + } + } + + private class OptionalCborString( + private val spec: FieldSpec, + ) : ReadWriteProperty { + override fun getValue(thisRef: MapBackedPerson, property: KProperty<*>): String? { + val element = thisRef.backing[CborString(spec.name, *spec.keyTags)] ?: return null + return (element as? CborString)?.value + ?: throw SerializationException("Expected '${spec.name}' to be CborString, got ${element::class}") + } + + override fun setValue(thisRef: MapBackedPerson, property: KProperty<*>, value: String?) { + if (value == null) { + thisRef.remove(spec.name, keyTags = spec.keyTags) + return + } + thisRef.put(spec.name, CborString(value, *spec.valueTags), keyTags = spec.keyTags) + } + } + + private var MapBackedPerson.gender: String? by OptionalCborString(GENDER_SPEC) + + private data class FieldSpec( + val name: String, + val label: Long?, + val keyTags: ULongArray = ulongArrayOf(), + val valueTags: ULongArray = ulongArrayOf(), + ) + + private object MapBackedPersonSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("MapBackedPerson") + + override fun serialize(encoder: Encoder, value: MapBackedPerson) { + val cborEncoder = encoder as? CborEncoder + ?: throw SerializationException("MapBackedPersonSerializer is intended for CBOR tests only.") + + val preferLabels = cborEncoder.cbor.configuration.preferCborLabelsOverNames + + val out = LinkedHashMap() + for ((key, v) in value.backing.entries) { + if (!preferLabels) { + out[key] = v + continue + } + + val label = when (key) { + is CborString -> when (key.value) { + NAME_SPEC.name -> NAME_SPEC.label + AGE_SPEC.name -> AGE_SPEC.label + GENDER_SPEC.name -> GENDER_SPEC.label + else -> null + } + + else -> null + } + + out[if (label == null) key else CborInt(label)] = v + } + cborEncoder.encodeCborElement(CborMap(out, *value.backing.tags)) + } + + override fun deserialize(decoder: Decoder): MapBackedPerson { + val cborDecoder = decoder as? CborDecoder + ?: throw SerializationException("MapBackedPersonSerializer is intended for CBOR tests only.") + + val element = cborDecoder.decodeCborElement() + val map = element as? CborMap + ?: throw SerializationException("Expected CborMap, got ${element::class}") + return MapBackedPerson.fromBacking(map) + } + } + + @Test + fun testRoundTripToBytes() { + val cbor = Cbor {} + val value = MapBackedPerson("Ada", 42).also { person -> + person.gender = "female" + person.put("country", CborString("AT")) + } + + val bytes = cbor.encodeToByteArray(MapBackedPersonSerializer, value) + val decoded = cbor.decodeFromByteArray(MapBackedPersonSerializer, bytes) + assertEquals(value, decoded) + assertEquals("Ada", decoded.name) + assertEquals(42, decoded.age) + assertEquals("female", decoded.gender) + assertEquals(CborString("AT"), decoded.backing["country"]) + } + + @Test + fun testRoundTripViaCborElement() { + val cbor = Cbor {} + val value = MapBackedPerson("Ada", 42).also { person -> + person.gender = "female" + person.put("country", CborString("AT")) + } + val element = cbor.encodeToCborElement(MapBackedPersonSerializer, value) + assertTrue(element is CborMap) + val decoded = cbor.decodeFromCborElement(MapBackedPersonSerializer, element) + assertEquals(value, decoded) + assertEquals("female", decoded.gender) + assertEquals(CborString("AT"), decoded.backing["country"]) + } + + @Test + fun testPreferLabelsOverNamesAffectsEncoding() { + val cbor = Cbor { preferCborLabelsOverNames = true } + val value = MapBackedPerson("Ada", 42).also { person -> + person.gender = "female" + person.put("country", CborString("AT")) + } + + val element = cbor.encodeToCborElement(MapBackedPersonSerializer, value) + assertTrue(element is CborMap) + assertEquals(CborString("Ada"), element.getValue(NAME_LABEL)) + assertEquals(CborInt(42), element.getValue(AGE_LABEL)) + assertEquals(CborString("female"), element.getValue(GENDER_LABEL)) + assertEquals(CborString("AT"), element.getValue("country")) + + // Decoder accepts both string keys and label keys, but normalizes back to string keys in the backing map. + val decoded = cbor.decodeFromCborElement(MapBackedPersonSerializer, element) + assertEquals("Ada", decoded.name) + assertEquals(42, decoded.age) + assertEquals("female", decoded.gender) + assertEquals(CborString("AT"), decoded.backing["country"]) + assertTrue(decoded.backing["name"] is CborString) + } +} From dd6a7429aaf1976a28419c26e6ae96865810aec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sun, 25 Jan 2026 14:13:59 +0100 Subject: [PATCH 61/68] fix CborInt function name --- .../kotlinx/serialization/cbor/CborElement.kt | 12 ++-- .../serialization/cbor/internal/Encoder.kt | 12 ++-- .../cbor/CborDelegatedPropertiesTest.kt | 6 +- .../cbor/CborElementEqualityTest.kt | 68 +++++++++---------- .../cbor/CborElementSerializerBehaviorTest.kt | 10 +-- .../serialization/cbor/CborElementTest.kt | 66 +++++++++--------- .../cbor/CborUnsignedInteroperabilityTest.kt | 4 +- 7 files changed, 89 insertions(+), 89 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt index 0984d0828c..91b91a1213 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -147,7 +147,7 @@ public class CborInteger( */ @ExperimentalSerializationApi @Suppress("FunctionName") -public fun CborInt(value: Long, vararg tags: ULong): CborInteger = +public fun CborInteger(value: Long, vararg tags: ULong): CborInteger = if (value >= 0L) CborInteger(value.toULong(), isPositive = true, tags = tags) else CborInteger(ULong.MAX_VALUE - value.toULong() + 1uL, isPositive = false, tags = tags) @@ -156,7 +156,7 @@ public fun CborInt(value: Long, vararg tags: ULong): CborInteger = */ @ExperimentalSerializationApi @Suppress("FunctionName") -public fun CborInt(value: ULong, vararg tags: ULong): CborInteger = +public fun CborInteger(value: ULong, vararg tags: ULong): CborInteger = CborInteger(value, isPositive = true, tags = tags) /** @@ -344,11 +344,11 @@ public class CborMap( public operator fun get(key: String): CborElement? = content[CborString(key)] public fun getValue(key: String): CborElement = content.getValue(CborString(key)) - public operator fun get(key: Long): CborElement? = content[CborInt(key)] - public fun getValue(key: Long): CborElement = content.getValue(CborInt(key)) + public operator fun get(key: Long): CborElement? = content[CborInteger(key)] + public fun getValue(key: Long): CborElement = content.getValue(CborInteger(key)) - public operator fun get(key: Int): CborElement? = content[CborInt(key.toLong())] - public fun getValue(key: Int): CborElement = content.getValue(CborInt(key.toLong())) + public operator fun get(key: Int): CborElement? = content[CborInteger(key.toLong())] + public fun getValue(key: Int): CborElement = content.getValue(CborInteger(key.toLong())) } 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 447fb9aa6a..47682a85a7 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -359,7 +359,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { //indices are put into the name field. we don't want to write those, as it would result in double writes val cborLabel = descriptor.getCborLabel(index) if (cbor.configuration.preferCborLabelsOverNames && cborLabel != null) { - currentElement += CborInt(value = cborLabel, tags = keyTags ?: EMPTY_TAGS) + currentElement += CborInteger(value = cborLabel, tags = keyTags ?: EMPTY_TAGS) } else { currentElement += CborString(name, tags = keyTags ?: EMPTY_TAGS) } @@ -388,12 +388,12 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { override fun encodeByte(value: Byte) { onDataItemEncoded() - currentElement += CborInt(value.toLong(), tags = takePendingValueTags()) + currentElement += CborInteger(value.toLong(), tags = takePendingValueTags()) } override fun encodeChar(value: Char) { onDataItemEncoded() - currentElement += CborInt(value.code.toLong(), tags = takePendingValueTags()) + currentElement += CborInteger(value.code.toLong(), tags = takePendingValueTags()) } override fun encodeDouble(value: Double) { @@ -408,12 +408,12 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { override fun encodeInt(value: Int) { onDataItemEncoded() - currentElement += CborInt(value.toLong(), tags = takePendingValueTags()) + currentElement += CborInteger(value.toLong(), tags = takePendingValueTags()) } override fun encodeLong(value: Long) { onDataItemEncoded() - currentElement += CborInt(value, tags = takePendingValueTags()) + currentElement += CborInteger(value, tags = takePendingValueTags()) } override fun encodeNegative(value: ULong) { @@ -429,7 +429,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { override fun encodeShort(value: Short) { onDataItemEncoded() - currentElement += CborInt(value.toLong(), tags = takePendingValueTags()) + currentElement += CborInteger(value.toLong(), tags = takePendingValueTags()) } override fun encodeString(value: String) { diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDelegatedPropertiesTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDelegatedPropertiesTest.kt index 5e8f4f0a1e..8c3265ffd1 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDelegatedPropertiesTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDelegatedPropertiesTest.kt @@ -126,7 +126,7 @@ class CborDelegatedPropertiesTest { } override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) { - content[CborString(spec.name, *spec.keyTags)] = CborInt(value.toLong(), *spec.valueTags) + content[CborString(spec.name, *spec.keyTags)] = CborInteger(value.toLong(), *spec.valueTags) } } @@ -184,7 +184,7 @@ class CborDelegatedPropertiesTest { else -> null } - out[if (label == null) key else CborInt(label)] = v + out[if (label == null) key else CborInteger(label)] = v } cborEncoder.encodeCborElement(CborMap(out, *value.backing.tags)) } @@ -243,7 +243,7 @@ class CborDelegatedPropertiesTest { val element = cbor.encodeToCborElement(MapBackedPersonSerializer, value) assertTrue(element is CborMap) assertEquals(CborString("Ada"), element.getValue(NAME_LABEL)) - assertEquals(CborInt(42), element.getValue(AGE_LABEL)) + assertEquals(CborInteger(42), element.getValue(AGE_LABEL)) assertEquals(CborString("female"), element.getValue(GENDER_LABEL)) assertEquals(CborString("AT"), element.getValue("country")) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt index 4af064e617..a969fb5d7f 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt @@ -8,10 +8,10 @@ class CborElementEqualityTest { @Test fun testCborPositiveIntEquality() { - val int1 = CborInt(42u) - val int2 = CborInt(42u) - val int3 = CborInt(43u) - val int4 = CborInt(42u, 1u) + val int1 = CborInteger(42u) + val int2 = CborInteger(42u) + val int3 = CborInteger(43u) + val int4 = CborInteger(42u, 1u) // Same values should be equal assertEquals(int1, int2) @@ -33,18 +33,18 @@ class CborElementEqualityTest { @Test fun testCborNegativeIntEquality() { - val int1 = CborInt(-42) - val int2 = CborInt(-42) - val int3 = CborInt(-43) - val int4 = CborInt(-42, 1u) + val int1 = CborInteger(-42) + val int2 = CborInteger(-42) + val int3 = CborInteger(-43) + val int4 = CborInteger(-42, 1u) assertEquals(int1, int2) assertEquals(int1.hashCode(), int2.hashCode()) assertNotEquals(int1, int3) assertNotEquals(int1, int4) assertNotEquals(int1, null as CborElement?) - assertNotEquals(int1, CborInt(42u) as CborElement) - assertNotEquals(int1 as CborElement, CborInt(42u)) + assertNotEquals(int1, CborInteger(42u) as CborElement) + assertNotEquals(int1 as CborElement, CborInteger(42u)) } @Test @@ -75,8 +75,8 @@ class CborElementEqualityTest { assertNotEquals(string1, string3) assertNotEquals(string1, string4) assertNotEquals(string1, null as CborElement?) - assertNotEquals(string1 as CborElement, CborInt(123u)) - assertNotEquals(string1, CborInt(123u) as CborElement) + assertNotEquals(string1 as CborElement, CborInteger(123u)) + assertNotEquals(string1, CborInteger(123u) as CborElement) } @Test @@ -131,11 +131,11 @@ class CborElementEqualityTest { @Test fun testCborArrayEquality() { - val list1 = CborArray(listOf(CborInt(1u), CborString("test"))) - val list2 = CborArray(listOf(CborInt(1u), CborString("test"))) - val list3 = CborArray(listOf(CborInt(2u), CborString("test"))) - val list4 = CborArray(listOf(CborInt(1u), CborString("test")), 1u) - val list5 = CborArray(listOf(CborInt(1u))) + val list1 = CborArray(listOf(CborInteger(1u), CborString("test"))) + val list2 = CborArray(listOf(CborInteger(1u), CborString("test"))) + val list3 = CborArray(listOf(CborInteger(2u), CborString("test"))) + val list4 = CborArray(listOf(CborInteger(1u), CborString("test")), 1u) + val list5 = CborArray(listOf(CborInteger(1u))) assertEquals(list1, list2) assertEquals(list1.hashCode(), list2.hashCode()) @@ -151,31 +151,31 @@ class CborElementEqualityTest { fun testCborMapEquality() { val map1 = CborMap( mapOf( - CborString("key1") to CborInt(1u), + CborString("key1") to CborInteger(1u), CborString("key2") to CborString("value") ) ) val map2 = CborMap( mapOf( - CborString("key1") to CborInt(1u), + CborString("key1") to CborInteger(1u), CborString("key2") to CborString("value") ) ) val map3 = CborMap( mapOf( - CborString("key1") to CborInt(2u), + CborString("key1") to CborInteger(2u), CborString("key2") to CborString("value") ) ) val map4 = CborMap( mapOf( - CborString("key1") to CborInt(1u), + CborString("key1") to CborInteger(1u), CborString("key2") to CborString("value") ), 1u ) val map5 = CborMap( mapOf( - CborString("key1") to CborInt(1u) + CborString("key1") to CborInteger(1u) ) ) @@ -225,7 +225,7 @@ class CborElementEqualityTest { mapOf( CborString("list") to CborArray( listOf( - CborInt(1u), + CborInteger(1u), CborMap(mapOf(CborString("inner") to CborNull())) ) ) @@ -235,7 +235,7 @@ class CborElementEqualityTest { mapOf( CborString("list") to CborArray( listOf( - CborInt(1u), + CborInteger(1u), CborMap(mapOf(CborString("inner") to CborNull())) ) ) @@ -245,7 +245,7 @@ class CborElementEqualityTest { mapOf( CborString("list") to CborArray( listOf( - CborInt(2u), + CborInteger(2u), CborMap(mapOf(CborString("inner") to CborNull())) ) ) @@ -260,15 +260,15 @@ class CborElementEqualityTest { @Test fun testReflexiveEquality() { val elements = listOf( - CborInt(42u), - CborInt(-42), + CborInteger(42u), + CborInteger(-42), CborFloat(3.14), CborString("test"), CborBoolean(true), CborByteString(byteArrayOf(1, 2, 3)), CborNull(), - CborArray(listOf(CborInt(1u))), - CborMap(mapOf(CborString("key") to CborInt(1u))) + CborArray(listOf(CborInteger(1u))), + CborMap(mapOf(CborString("key") to CborInteger(1u))) ) elements.forEach { element -> @@ -280,17 +280,17 @@ class CborElementEqualityTest { @Test fun testSymmetricEquality() { val pairs = listOf( - CborInt(42u) to CborInt(42u), - CborInt(-42) to CborInt(-42), + CborInteger(42u) to CborInteger(42u), + CborInteger(-42) to CborInteger(-42), CborFloat(3.14) to CborFloat(3.14), CborString("test") to CborString("test"), CborBoolean(true) to CborBoolean(true), CborByteString(byteArrayOf(1, 2, 3)) to CborByteString(byteArrayOf(1, 2, 3)), CborNull() to CborNull(), - CborArray(listOf(CborInt(1u))) to CborArray(listOf(CborInt(1u))), - CborMap(mapOf(CborString("key") to CborInt(1u))) to CborMap( + CborArray(listOf(CborInteger(1u))) to CborArray(listOf(CborInteger(1u))), + CborMap(mapOf(CborString("key") to CborInteger(1u))) to CborMap( mapOf( - CborString("key") to CborInt( + CborString("key") to CborInteger( 1u ) ) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementSerializerBehaviorTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementSerializerBehaviorTest.kt index 1f78982cfa..5094c5d992 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementSerializerBehaviorTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementSerializerBehaviorTest.kt @@ -57,7 +57,7 @@ class CborElementSerializerBehaviorTest { @Test fun cborElementSerializerRequiresCborEncoder() { val ex = assertFailsWith { - CborElement.serializer().serialize(NonCborEncoder, CborInt(1)) + CborElement.serializer().serialize(NonCborEncoder, CborInteger(1)) } assertTrue(ex.message?.contains("This serializer can be used only with Cbor format") == true) } @@ -73,24 +73,24 @@ class CborElementSerializerBehaviorTest { @Test fun typedCborArrayDeserializationFailsOnNonListInput() { val cbor = Cbor {} - val bytes = cbor.encodeToByteArray(CborElement.serializer(), CborInt(1)) + val bytes = cbor.encodeToByteArray(CborElement.serializer(), CborInteger(1)) assertFailsWith { cbor.decodeFromByteArray(CborArray.serializer(), bytes) } assertFailsWith { - cbor.decodeFromCborElement(CborArray.serializer(), CborInt(1)) + cbor.decodeFromCborElement(CborArray.serializer(), CborInteger(1)) } } @Test fun typedCborIntDeserializationFailsOnNonIntInput() { val cbor = Cbor {} - val bytes = cbor.encodeToByteArray(CborElement.serializer(), CborArray(listOf(CborInt(1)))) + val bytes = cbor.encodeToByteArray(CborElement.serializer(), CborArray(listOf(CborInteger(1)))) assertFailsWith { cbor.decodeFromByteArray(CborInteger.serializer(), bytes) } assertFailsWith { - cbor.decodeFromCborElement(CborInteger.serializer(), CborArray(listOf(CborInt(1)))) + cbor.decodeFromCborElement(CborInteger.serializer(), CborArray(listOf(CborInteger(1)))) } } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index 785e2bd510..70964a019d 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -12,7 +12,7 @@ class CborElementTest { @Test fun testEncodeToCborElementRootPrimitiveInt() { val element = cbor.encodeToCborElement(42) - assertEquals(CborInt(42), element) + assertEquals(CborInteger(42), element) assertEquals(42, cbor.decodeFromCborElement(element)) } @@ -59,7 +59,7 @@ class CborElementTest { @Test fun testCborNumber() { - val numberElement = CborInt(42u) + val numberElement = CborInteger(42u) val numberBytes = cbor.encodeToByteArray(numberElement) val decodedNumber = cbor.decodeFromByteArray(numberBytes) assertEquals(numberElement, decodedNumber) @@ -68,8 +68,8 @@ class CborElementTest { @Test fun testCborNumberZero() { - val numberElement = CborInt(0uL) - assertEquals(numberElement, CborInt(0)) + val numberElement = CborInteger(0uL) + assertEquals(numberElement, CborInteger(0)) assertEquals(numberElement.isPositive, true) assertEquals(numberElement.value, 0uL) val numberBytes = cbor.encodeToByteArray(numberElement) @@ -80,7 +80,7 @@ class CborElementTest { @Test fun testCborNumberMax() { - val numberElement = CborInt(ULong.MAX_VALUE) + val numberElement = CborInteger(ULong.MAX_VALUE) assertEquals(numberElement.isPositive, true) assertEquals(numberElement.value, ULong.MAX_VALUE) val numberBytes = cbor.encodeToByteArray(numberElement) @@ -91,7 +91,7 @@ class CborElementTest { @Test fun testCborNumberMaxHalv() { - val numberElement = CborInt(Long.MAX_VALUE) + val numberElement = CborInteger(Long.MAX_VALUE) assertEquals(numberElement.isPositive, true) assertEquals(numberElement.value, Long.MAX_VALUE.toULong()) val numberBytes = cbor.encodeToByteArray(numberElement) @@ -137,8 +137,8 @@ class CborElementTest { @Test fun testCborNumberLong() { - assertEquals(Long.MAX_VALUE, CborInt(Long.MAX_VALUE).long) - assertEquals(Long.MIN_VALUE, CborInt(Long.MIN_VALUE).long) + assertEquals(Long.MAX_VALUE, CborInteger(Long.MAX_VALUE).long) + assertEquals(Long.MIN_VALUE, CborInteger(Long.MIN_VALUE).long) } @Test @@ -173,7 +173,7 @@ class CborElementTest { fun testCborArray() { val listElement = CborArray( listOf( - CborInt(1u), + CborInteger(1u), CborString("two"), CborBoolean(true), CborNull() @@ -203,9 +203,9 @@ class CborElementTest { fun testCborMap() { val mapElement = CborMap( mapOf( - CborString("key1") to CborInt(42u), + CborString("key1") to CborInteger(42u), CborString("key2") to CborString("value"), - CborInt(3u) to CborBoolean(true), + CborInteger(3u) to CborBoolean(true), CborNull() to CborNull() ) ) @@ -232,8 +232,8 @@ class CborElementTest { assertTrue(value2 is CborString) assertEquals("value", (value2 as CborString).value) - assertTrue(decodedMap.containsKey(CborInt(3u))) - val value3 = decodedMap[CborInt(3u)] + assertTrue(decodedMap.containsKey(CborInteger(3u))) + val value3 = decodedMap[CborInteger(3u)] assertTrue(value3 is CborBoolean) assertEquals(true, (value3 as CborBoolean).value) @@ -249,7 +249,7 @@ class CborElementTest { mapOf( CborString("primitives") to CborArray( listOf( - CborInt(123u), + CborInteger(123u), CborString("text"), CborBoolean(false), CborByteString(byteArrayOf(10, 20, 30)), @@ -260,8 +260,8 @@ class CborElementTest { mapOf( CborString("inner") to CborArray( listOf( - CborInt(1u), - CborInt(2u) + CborInteger(1u), + CborInteger(2u) ) ), CborString("empty") to CborArray(emptyList()) @@ -417,9 +417,9 @@ class CborElementTest { // Check the nested map val nestedMap = map[CborString("d")] as CborMap assertEquals(3, nestedMap.size) - assertEquals(CborInt(1u), nestedMap[CborString("1")]) - assertEquals(CborInt(2u), nestedMap[CborString("2")]) - assertEquals(CborInt(3u), nestedMap[CborString("3")]) + assertEquals(CborInteger(1u), nestedMap[CborString("1")]) + assertEquals(CborInteger(2u), nestedMap[CborString("2")]) + assertEquals(CborInteger(3u), nestedMap[CborString("3")]) } @OptIn(ExperimentalStdlibApi::class) @@ -454,8 +454,8 @@ class CborElementTest { str = "A string, is a string, is a string", bStr = CborByteString(byteArrayOf()), cborElement = CborBoolean(false), - cborPositiveInt = CborInt(1u), - cborInt = CborInt(-1), + cborPositiveInt = CborInteger(1u), + cborInt = CborInteger(-1), tagged = 26 ), "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472406b63626f72456c656d656e74f46f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564d90921181aff" @@ -481,8 +481,8 @@ class CborElementTest { str = "A string, is a string, is a string", bStr = null, cborElement = CborBoolean(false), - cborPositiveInt = CborInt(1u), - cborInt = CborInt(-1), + cborPositiveInt = CborInteger(1u), + cborInt = CborInteger(-1), tagged = 26 ), "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472f66b63626f72456c656d656e74f46f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564d90921181aff" @@ -509,8 +509,8 @@ class CborElementTest { str = "A string, is a string, is a string", bStr = null, cborElement = CborMap(mapOf(CborByteString(byteArrayOf(1, 3, 3, 7)) to CborNull())), - cborPositiveInt = CborInt(1u), - cborInt = CborInt(-1), + cborPositiveInt = CborInteger(1u), + cborInt = CborInteger(-1), tagged = 26 ), "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472f66b63626f72456c656d656e74bf4401030307f6ff6f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564d90921181aff" @@ -538,8 +538,8 @@ class CborElementTest { str = "A string, is a string, is a string", bStr = null, cborElement = CborNull(), - cborPositiveInt = CborInt(1u), - cborInt = CborInt(-1), + cborPositiveInt = CborInteger(1u), + cborInt = CborInteger(-1), tagged = 26 ), "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472f66b63626f72456c656d656e74f66f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564d90921181aff" @@ -566,8 +566,8 @@ class CborElementTest { str = "A string, is a string, is a string", bStr = CborByteString(byteArrayOf(), 1u, 3u), cborElement = CborBoolean(false), - cborPositiveInt = CborInt(1u), - cborInt = CborInt(-1), + cborPositiveInt = CborInteger(1u), + cborInt = CborInteger(-1), tagged = 26 ), "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472c1c3406b63626f72456c656d656e74f46f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564d90921181aff" @@ -593,8 +593,8 @@ class CborElementTest { str = "A string, is a string, is a string", bStr = CborByteString(byteArrayOf(), 1u, 3u), cborElement = CborBoolean(false), - cborPositiveInt = CborInt(1u), - cborInt = CborInt(-1), + cborPositiveInt = CborInteger(1u), + cborInt = CborInteger(-1), tagged = 26 ), "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472c1c3406b63626f72456c656d656e74f46f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564181aff" @@ -738,10 +738,10 @@ class CborElementTest { @OptIn(ExperimentalUnsignedTypes::class) @Test fun testTagsPreservedWhenDecodingTypedElements() { - val taggedMap = CborMap(mapOf(CborString("a") to CborInt(1)), 1uL) + val taggedMap = CborMap(mapOf(CborString("a") to CborInteger(1)), 1uL) assertEquals(taggedMap, cbor.decodeFromByteArray(cbor.encodeToByteArray(taggedMap))) - val taggedList = CborArray(listOf(CborInt(1)), 2uL) + val taggedList = CborArray(listOf(CborInteger(1)), 2uL) assertEquals(taggedList, cbor.decodeFromByteArray(cbor.encodeToByteArray(taggedList))) val taggedFloat = CborFloat(1.5, 3uL) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborUnsignedInteroperabilityTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborUnsignedInteroperabilityTest.kt index 486f81e156..e445c93814 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborUnsignedInteroperabilityTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborUnsignedInteroperabilityTest.kt @@ -29,7 +29,7 @@ class CborUnsignedInteroperabilityTest { assertEquals(canonicalULongMax, Cbor.encodeToHexString(ULong.MAX_VALUE)) // Structured decoding from CborElement also supports unsigned values beyond Long range. - assertEquals(ULong.MAX_VALUE, Cbor.decodeFromCborElement(CborInt(ULong.MAX_VALUE))) + assertEquals(ULong.MAX_VALUE, Cbor.decodeFromCborElement(CborInteger(ULong.MAX_VALUE))) } @Test @@ -42,6 +42,6 @@ class CborUnsignedInteroperabilityTest { assertFailsWith { Cbor.decodeFromHexString("20") } // -1 -> ULong.MAX_VALUE // Structured decoding should also reject negative integers for unsigned Kotlin types. - assertFailsWith { Cbor.decodeFromCborElement(CborInt(-1)) } + assertFailsWith { Cbor.decodeFromCborElement(CborInteger(-1)) } } } From 078d2a5f8047d8b2e867c00ac56593fa934a06da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Tue, 19 May 2026 17:53:31 +0200 Subject: [PATCH 62/68] CBOR: remove nullAsMap --- docs/formats.md | 5 - .../cbor/api/kotlinx-serialization-cbor.api | 11 +- .../api/kotlinx-serialization-cbor.klib.api | 8 +- .../serialization/cbor/CborNullAsEmptyMap.kt | 42 ----- .../serialization/cbor/internal/Encoder.kt | 14 +- .../cbor/CborNullAsEmptyMapTest.kt | 178 ------------------ 6 files changed, 6 insertions(+), 252 deletions(-) delete mode 100644 formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborNullAsEmptyMap.kt delete mode 100644 formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborNullAsEmptyMapTest.kt diff --git a/docs/formats.md b/docs/formats.md index 2b0449e3aa..a4cbf82a9a 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -314,11 +314,6 @@ When annotated with `@CborObjectAsArray`, serialization of the same object will ``` This may be used to encode COSE structures, see [RFC 9052 2. Basic COSE Structure](https://www.rfc-editor.org/rfc/rfc9052#section-2). -### Nullability of Properties -Some standards, like COSE, tend to encode the absence of a complex property as an empty map (because the complex property itself -consists only of nullable properties). This cannot be modelled elegantly, such that the null-safety of Kotlin can be leveraged. -To work around this, complex nullable properties can be annotated with [`@CborNullAsEmptyMap`](CborNullAsEmptyMap.kt), to emulate this behaviour. - ### Custom CBOR-specific Serializers Cbor encoders and decoders implement the interfaces [CborEncoder](CborEncoder.kt) and [CborDecoder](CborDecoder.kt), respectively. These interfaces contain a single property, `cbor`, exposing the current CBOR serialization configuration. diff --git a/formats/cbor/api/kotlinx-serialization-cbor.api b/formats/cbor/api/kotlinx-serialization-cbor.api index e27bdb52ea..8437194bf9 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.api @@ -157,8 +157,8 @@ public final class kotlinx/serialization/cbor/CborElement$Companion { } public final class kotlinx/serialization/cbor/CborElementKt { - public static final fun CborInt-SIFponk (J[J)Lkotlinx/serialization/cbor/CborInteger; - public static final fun CborInt-ahITK_k (J[J)Lkotlinx/serialization/cbor/CborInteger; + public static final fun CborInteger-SIFponk (J[J)Lkotlinx/serialization/cbor/CborInteger; + public static final fun CborInteger-ahITK_k (J[J)Lkotlinx/serialization/cbor/CborInteger; public static final fun getByte (Lkotlinx/serialization/cbor/CborInteger;)B public static final fun getByteOrNull (Lkotlinx/serialization/cbor/CborInteger;)Ljava/lang/Byte; public static final fun getInt (Lkotlinx/serialization/cbor/CborInteger;)I @@ -294,13 +294,6 @@ public final class kotlinx/serialization/cbor/CborNull$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public abstract interface annotation class kotlinx/serialization/cbor/CborNullAsEmptyMap : java/lang/annotation/Annotation { -} - -public final synthetic class kotlinx/serialization/cbor/CborNullAsEmptyMap$Impl : kotlinx/serialization/cbor/CborNullAsEmptyMap { - public fun ()V -} - public abstract interface annotation class kotlinx/serialization/cbor/CborObjectAsArray : java/lang/annotation/Annotation { } diff --git a/formats/cbor/api/kotlinx-serialization-cbor.klib.api b/formats/cbor/api/kotlinx-serialization-cbor.klib.api index 3377428921..0ce3d8de4d 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.klib.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.klib.api @@ -17,10 +17,6 @@ open annotation class kotlinx.serialization.cbor/CborLabel : kotlin/Annotation { final fun (): kotlin/Long // kotlinx.serialization.cbor/CborLabel.label.|(){}[0] } -open annotation class kotlinx.serialization.cbor/CborNullAsEmptyMap : kotlin/Annotation { // kotlinx.serialization.cbor/CborNullAsEmptyMap|null[0] - constructor () // kotlinx.serialization.cbor/CborNullAsEmptyMap.|(){}[0] -} - open annotation class kotlinx.serialization.cbor/CborObjectAsArray : kotlin/Annotation { // kotlinx.serialization.cbor/CborObjectAsArray|null[0] constructor () // kotlinx.serialization.cbor/CborObjectAsArray.|(){}[0] } @@ -377,7 +373,7 @@ final val kotlinx.serialization.cbor/shortOrNull // kotlinx.serialization.cbor/s final fun (kotlinx.serialization.cbor/CborInteger).(): kotlin/Short? // kotlinx.serialization.cbor/shortOrNull.|@kotlinx.serialization.cbor.CborInteger(){}[0] final fun kotlinx.serialization.cbor/Cbor(kotlinx.serialization.cbor/Cbor = ..., kotlin/Function1): kotlinx.serialization.cbor/Cbor // kotlinx.serialization.cbor/Cbor|Cbor(kotlinx.serialization.cbor.Cbor;kotlin.Function1){}[0] -final fun kotlinx.serialization.cbor/CborInt(kotlin/Long, kotlin/ULongArray...): kotlinx.serialization.cbor/CborInteger // kotlinx.serialization.cbor/CborInt|CborInt(kotlin.Long;kotlin.ULongArray...){}[0] -final fun kotlinx.serialization.cbor/CborInt(kotlin/ULong, kotlin/ULongArray...): kotlinx.serialization.cbor/CborInteger // kotlinx.serialization.cbor/CborInt|CborInt(kotlin.ULong;kotlin.ULongArray...){}[0] +final fun kotlinx.serialization.cbor/CborInteger(kotlin/Long, kotlin/ULongArray...): kotlinx.serialization.cbor/CborInteger // kotlinx.serialization.cbor/CborInteger|CborInteger(kotlin.Long;kotlin.ULongArray...){}[0] +final fun kotlinx.serialization.cbor/CborInteger(kotlin/ULong, kotlin/ULongArray...): kotlinx.serialization.cbor/CborInteger // kotlinx.serialization.cbor/CborInteger|CborInteger(kotlin.ULong;kotlin.ULongArray...){}[0] final inline fun <#A: reified kotlin/Any?> (kotlinx.serialization.cbor/Cbor).kotlinx.serialization.cbor/decodeFromCborElement(kotlinx.serialization.cbor/CborElement): #A // kotlinx.serialization.cbor/decodeFromCborElement|decodeFromCborElement@kotlinx.serialization.cbor.Cbor(kotlinx.serialization.cbor.CborElement){0§}[0] final inline fun <#A: reified kotlin/Any?> (kotlinx.serialization.cbor/Cbor).kotlinx.serialization.cbor/encodeToCborElement(#A): kotlinx.serialization.cbor/CborElement // kotlinx.serialization.cbor/encodeToCborElement|encodeToCborElement@kotlinx.serialization.cbor.Cbor(0:0){0§}[0] diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborNullAsEmptyMap.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborNullAsEmptyMap.kt deleted file mode 100644 index 03b46f089a..0000000000 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborNullAsEmptyMap.kt +++ /dev/null @@ -1,42 +0,0 @@ -package kotlinx.serialization.cbor - -import kotlinx.serialization.* - -/** - * Marks a complex property to be encoded as an empty map when null, instead of CBOR `null`. - * - * This is useful for COSE encoding, because COSE known protected and unprotected headers, for example, - * and the compiler handles null checks, while checks for empty maps would lead to duplicated spaghetti code. - * - * Example usage: - * - * ``` - * - * @Serializable - * data class ClassWNullableAsMap( - * @SerialName("nullable") - * @CborNullAsEmptyMap - * val nullable: NullableClass? - * ) - * - * @Serializable - * data class NullableClass( - * val property: String - * ) - * - * Cbor.encodeToByteArray(ClassWNullableAsMap(nullable = null)) - * ``` - * - * will produce bytes `0xbf686e756c6c61626c65a0ff`, or in diagnostic notation: - * - * ``` - *a1 # map(1) - * 68 # text(8) - * 6e756c6c61626c65 # "nullable" - * a0 # map(0) - * ``` - */ -@SerialInfo -@Target(AnnotationTarget.PROPERTY) -@ExperimentalSerializationApi -public annotation class CborNullAsEmptyMap 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 47682a85a7..aad225a2d7 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -63,8 +63,6 @@ internal sealed class CborWriter( getDestination().encodeUndefined() } - protected var encodeNullAsEmptyMap = false - protected var encodeByteArrayAsByteString = false class Data(val bytes: ByteArrayOutput, var elementCount: Int) @@ -167,8 +165,7 @@ internal sealed class CborWriter( override fun encodeNull() { onDataItemEncoded() - if (encodeNullAsEmptyMap) getDestination().encodeEmptyMap() - else getDestination().encodeNull() + getDestination().encodeNull() } @OptIn(ExperimentalSerializationApi::class) // KT-46731 @@ -182,7 +179,6 @@ internal sealed class CborWriter( override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean { val destination = getDestination() - encodeNullAsEmptyMap = descriptor.getElementAnnotations(index).find { it is CborNullAsEmptyMap } != null encodeByteArrayAsByteString = descriptor.isByteString(index) val name = descriptor.getElementName(index) @@ -347,8 +343,6 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean { - encodeNullAsEmptyMap = descriptor.getElementAnnotations(index).find { it is CborNullAsEmptyMap } != null - encodeByteArrayAsByteString = descriptor.isByteString(index) //TODO check if cborelement and be done val name = descriptor.getElementName(index) @@ -450,11 +444,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { override fun encodeNull() { onDataItemEncoded() val tags = takePendingValueTags() - currentElement += if (encodeNullAsEmptyMap) CborMap( - mapOf(), - tags = tags - ) - else CborNull(tags = tags) + currentElement += CborNull(tags = tags) } override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) { diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborNullAsEmptyMapTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborNullAsEmptyMapTest.kt deleted file mode 100644 index 445f97e962..0000000000 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborNullAsEmptyMapTest.kt +++ /dev/null @@ -1,178 +0,0 @@ -package kotlinx.serialization.cbor - -import kotlinx.serialization.* -import kotlin.test.* - - -class CborNullAsEmptyMapTest { - - - @Test - fun nullableAsMap() { - /** - * a1 # map(1) - * 68 # text(8) - * 6e756c6c61626c65 # "nullable" - * a0 # map(0) - */ - val referenceHexString = "a1686e756c6c61626c65a0" - val reference = ClassWNullableAsMap(nullable = null) - - val cbor = Cbor.CoseCompliant - - assertEquals(referenceHexString, cbor.encodeToHexString(ClassWNullableAsMap.serializer(), reference)) - assertEquals(reference, cbor.decodeFromHexString(ClassWNullableAsMap.serializer(), referenceHexString)) - } - - @Test - fun nullableAsNull() { - /** - * a1 # map(1) - * 68 # text(8) - * 6e756c6c61626c65 # "nullable" - * f6 # null, simple(22) - */ - val referenceHexString = "a1686e756c6c61626c65f6" - val reference = ClassWNullableAsNull(nullable = null) - - - val cbor = Cbor.CoseCompliant - - assertEquals(referenceHexString, cbor.encodeToHexString(ClassWNullableAsNull.serializer(), reference)) - assertEquals(reference, cbor.decodeFromHexString(ClassWNullableAsNull.serializer(), referenceHexString)) - } - - @Test - fun nullableAsMapWithDefaultNull() { - /** - * a1 # map(1) - * 68 # text(8) - * 6e756c6c61626c65 # "nullable" - * a0 # map(0) - */ - val referenceHexString = "a1686e756c6c61626c65a0" - val reference = ClassWNullableAsMapWithDefaultValueNull() - - val cbor = Cbor { - useDefiniteLengthEncoding = true - encodeDefaults = true - } - - assertEquals( - referenceHexString, - cbor.encodeToHexString(ClassWNullableAsMapWithDefaultValueNull.serializer(), reference) - ) - assertEquals( - reference, - cbor.decodeFromHexString(ClassWNullableAsMapWithDefaultValueNull.serializer(), referenceHexString) - ) - } - - @Test - fun nullableAsNullWithDefaultNull() { - /** - * a1 # map(1) - * 68 # text(8) - * 6e756c6c61626c65 # "nullable" - * f6 # null, simple(22) - */ - val referenceHexString = "a1686e756c6c61626c65f6" - val reference = ClassWNullableAsNullWithDefaultValueNull() - - - val cbor = Cbor { - useDefiniteLengthEncoding = true - encodeDefaults = true - } - - assertEquals( - referenceHexString, - cbor.encodeToHexString(ClassWNullableAsNullWithDefaultValueNull.serializer(), reference) - ) - assertEquals( - reference, - cbor.decodeFromHexString(ClassWNullableAsNullWithDefaultValueNull.serializer(), referenceHexString) - ) - } - @Test - fun nullableAsMapWithDefaultNullNoEncodeDefaults() { - /** - * a0 # map(0) - */ - val referenceHexString = "a0" - val reference = ClassWNullableAsMapWithDefaultValueNull() - - val cbor = Cbor { - useDefiniteLengthEncoding = true - encodeDefaults = false - } - - assertEquals( - referenceHexString, - cbor.encodeToHexString(ClassWNullableAsMapWithDefaultValueNull.serializer(), reference) - ) - assertEquals( - reference, - cbor.decodeFromHexString(ClassWNullableAsMapWithDefaultValueNull.serializer(), referenceHexString) - ) - } - - @Test - fun nullableAsNullWithDefaultNullNoEncodeDefaults() { - /** - * a0 # map(0) - */ - val referenceHexString = "a0" - val reference = ClassWNullableAsNullWithDefaultValueNull() - - - val cbor = Cbor { - useDefiniteLengthEncoding = true - encodeDefaults = false - } - - assertEquals( - referenceHexString, - cbor.encodeToHexString(ClassWNullableAsNullWithDefaultValueNull.serializer(), reference) - ) - assertEquals( - reference, - cbor.decodeFromHexString(ClassWNullableAsNullWithDefaultValueNull.serializer(), referenceHexString) - ) - } -} - -@Serializable -data class ClassWNullableAsMap( - @SerialName("nullable") - @CborNullAsEmptyMap - val nullable: NullableClass? -) - -@Serializable -data class ClassWNullableAsMapWithDefaultValueNull( - @SerialName("nullable") - @CborNullAsEmptyMap - val nullable: NullableClass? = null -) - - -@Serializable -data class ClassWNullableAsNull( - @SerialName("nullable") - val nullable: NullableClass? -) - -@Serializable -data class ClassWNullableAsNullWithDefaultValueNull( - @SerialName("nullable") - val nullable: NullableClass? = null -) - -@Serializable -data class NullableClass( - val property: String -) - - - From a2ca318b42468b26c1843b61f72613b1cc383160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Tue, 19 May 2026 18:41:24 +0200 Subject: [PATCH 63/68] remove abstract value from base CBOR element --- docs/formats.md | 11 +- docs/serialization-guide.md | 1 - .../cbor/api/kotlinx-serialization-cbor.api | 38 ++--- .../api/kotlinx-serialization-cbor.klib.api | 37 +++-- .../kotlinx/serialization/cbor/CborElement.kt | 142 ++++++++++++------ .../cbor/internal/CborElementSerializers.kt | 6 +- .../serialization/cbor/internal/Decoder.kt | 2 +- .../serialization/cbor/internal/Unsigned.kt | 2 +- .../cbor/CborDelegatedPropertiesTest.kt | 6 +- .../serialization/cbor/CborElementTest.kt | 50 +++--- .../cbor/CborUnsignedInteroperabilityTest.kt | 2 +- 11 files changed, 175 insertions(+), 122 deletions(-) diff --git a/docs/formats.md b/docs/formats.md index a4cbf82a9a..7b489f43dc 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -16,7 +16,6 @@ stable, these are currently experimental features of Kotlin Serialization. * [Definite vs. Indefinite Length Encoding](#definite-vs-indefinite-length-encoding) * [Tags and Labels](#tags-and-labels) * [Arrays](#arrays) - * [Nullability of Properties](#nullability-of-properties) * [Custom CBOR-specific Serializers](#custom-cbor-specific-serializers) * [CBOR Elements](#cbor-elements) * [Encoding from/to `CborElement`](#encoding-fromto-cborelement) @@ -349,7 +348,7 @@ fun main() { The above snippet will print the following diagnostic notation ```text -CborMap(tags=[], content={CborString(tags=[], value=bytes)=CborByteString(tags=[], value=h'666f6f)}) +CborMap(tags=[], content={CborString(tags=[], value=bytes)=CborByteString(tags=[], bytes=h'666f6f)}) ``` #### Tagging `CborElement`s @@ -394,7 +393,7 @@ Decoding it results in the following CborElement (shown in manually formatted di CborMap(tags=[], content={ CborString(tags=[], value=a) = CborPositiveInt( tags=[12], value=268435455), CborString(tags=[34], value=b) = CborNegativeInt( tags=[], value=-1), - CborString(tags=[56], value=c) = CborByteString( tags=[78], value=h'cafe), + CborString(tags=[56], value=c) = CborByteString( tags=[78], bytes=h'cafe), CborString(tags=[], value=d) = CborString( tags=[90, 12], value=Hello World) }) ``` @@ -412,14 +411,13 @@ A [CborElement] class has three direct subtypes, closely following CBOR grammar: * [CborPrimitive] represents primitive CBOR elements, such as string, integer, float, boolean, and null. CBOR byte strings are also treated as primitives. - Each primitive has a [value][CborPrimitive.value]. Depending on the concrete type of the primitive, it maps - to corresponding Kotlin Types such as `String`, `Long`, `Double`, etc. + Concrete primitive types expose dedicated accessors where their CBOR content maps cleanly to Kotlin values. Note that Cbor discriminates between positive ("unsigned") and negative ("signed") integers! `CborPrimitive` is itself an umbrella type (a sealed class) for the following concrete primitives: * [CborNull] mapping to a Kotlin `null` * [CborBoolean] mapping to a Kotlin `Boolean` * [CborInt] represents signed CBOR integer (major type 1 encompassing `-2^64..-1`) and unsigned CBOR integer (major type 0 encompassing `0..2^64-1`). - Since this exceeds the range of Kotlin's built-in `Long` type, CborInt consists of `sign` (set to `CborInt.Sing.POSITIVE`, `CborInt.Sing.NEGATIVE`, or `CborInt.Sing.ZERO`) and `value` representing the absolute value as an `ULong`. It also features a `toLong()` function, albeit incurring possible truncation for negative values exceeding `Long.MIN_VALUE`. + Since this exceeds the range of Kotlin's built-in `Long` type, CborInt consists of `isPositive` and `absoluteValue` representing the absolute value as an `ULong`. It also features `long`, `int`, `short`, and `byte` conversion properties that throw when the value cannot be represented in the requested Kotlin type. * [CborString] maps to a Kotlin `String` * [CborFloat] maps to Kotlin `Double` * [CborByteString] maps to a Kotlin `ByteArray` and is used to encode them as CBOR byte string (in contrast to a list @@ -1794,7 +1792,6 @@ This chapter concludes [Kotlin Serialization Guide](serialization-guide.md). [CborPrimitive]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-primitive/index.html [CborNull]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-null/index.html [CborBoolean]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-boolean/index.html -[CborInt]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-int.html [CborString]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-string/index.html [CborFloat]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-float/index.html [CborByteString]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-byte-string/index.html diff --git a/docs/serialization-guide.md b/docs/serialization-guide.md index 312dee62a1..d03f518f01 100644 --- a/docs/serialization-guide.md +++ b/docs/serialization-guide.md @@ -154,7 +154,6 @@ Once the project is set up, we can start serializing some classes. * [Definite vs. Indefinite Length Encoding](formats.md#definite-vs-indefinite-length-encoding) * [Tags and Labels](formats.md#tags-and-labels) * [Arrays](formats.md#arrays) - * [Nullability of Properties](formats.md#nullability-of-properties) * [Custom CBOR-specific Serializers](formats.md#custom-cbor-specific-serializers) * [CBOR Elements](formats.md#cbor-elements) * [Encoding from/to `CborElement`](formats.md#encoding-fromto-cborelement) diff --git a/formats/cbor/api/kotlinx-serialization-cbor.api b/formats/cbor/api/kotlinx-serialization-cbor.api index 8437194bf9..564aadb8eb 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.api @@ -69,8 +69,10 @@ public final class kotlinx/serialization/cbor/CborArray$Companion { public final class kotlinx/serialization/cbor/CborBoolean : kotlinx/serialization/cbor/CborPrimitive { public static final field Companion Lkotlinx/serialization/cbor/CborBoolean$Companion; public synthetic fun (Z[JLkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun getValue ()Ljava/lang/Boolean; - public synthetic fun getValue ()Ljava/lang/Object; + public fun equals (Ljava/lang/Object;)Z + public final fun getValue ()Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class kotlinx/serialization/cbor/CborBoolean$Companion { @@ -108,9 +110,8 @@ public final class kotlinx/serialization/cbor/CborByteString : kotlinx/serializa public static final field Companion Lkotlinx/serialization/cbor/CborByteString$Companion; public synthetic fun ([B[JLkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z - public synthetic fun getValue ()Ljava/lang/Object; - public fun getValue ()[B public fun hashCode ()I + public final fun toByteArray ()[B public fun toString ()Ljava/lang/String; } @@ -190,8 +191,10 @@ public final class kotlinx/serialization/cbor/CborEncoder$DefaultImpls { public final class kotlinx/serialization/cbor/CborFloat : kotlinx/serialization/cbor/CborPrimitive { public static final field Companion Lkotlinx/serialization/cbor/CborFloat$Companion; public synthetic fun (D[JLkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun getValue ()Ljava/lang/Double; - public synthetic fun getValue ()Ljava/lang/Object; + public fun equals (Ljava/lang/Object;)Z + public final fun getValue ()D + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class kotlinx/serialization/cbor/CborFloat$Companion { @@ -202,8 +205,7 @@ public final class kotlinx/serialization/cbor/CborInteger : kotlinx/serializatio public static final field Companion Lkotlinx/serialization/cbor/CborInteger$Companion; public synthetic fun (JZ[JLkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z - public synthetic fun getValue ()Ljava/lang/Object; - public fun getValue-s-VKNKU ()J + public final fun getAbsoluteValue-s-VKNKU ()J public fun hashCode ()I public final fun isPositive ()Z public fun toString ()Ljava/lang/String; @@ -286,8 +288,9 @@ public final class kotlinx/serialization/cbor/CborMap$Companion { public final class kotlinx/serialization/cbor/CborNull : kotlinx/serialization/cbor/CborPrimitive { public static final field Companion Lkotlinx/serialization/cbor/CborNull$Companion; public synthetic fun ([JLkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun getValue ()Ljava/lang/Object; - public fun getValue ()Lkotlin/Unit; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class kotlinx/serialization/cbor/CborNull$Companion { @@ -305,10 +308,6 @@ public abstract class kotlinx/serialization/cbor/CborPrimitive : kotlinx/seriali public static final field Companion Lkotlinx/serialization/cbor/CborPrimitive$Companion; public synthetic fun ([JILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun ([JLkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun equals (Ljava/lang/Object;)Z - protected abstract fun getValue ()Ljava/lang/Object; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; } public final class kotlinx/serialization/cbor/CborPrimitive$Companion { @@ -318,8 +317,10 @@ public final class kotlinx/serialization/cbor/CborPrimitive$Companion { public final class kotlinx/serialization/cbor/CborString : kotlinx/serialization/cbor/CborPrimitive { public static final field Companion Lkotlinx/serialization/cbor/CborString$Companion; public synthetic fun (Ljava/lang/String;[JLkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun getValue ()Ljava/lang/Object; - public fun getValue ()Ljava/lang/String; + public fun equals (Ljava/lang/Object;)Z + public final fun getValue ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class kotlinx/serialization/cbor/CborString$Companion { @@ -350,8 +351,9 @@ public final class kotlinx/serialization/cbor/CborTag { public final class kotlinx/serialization/cbor/CborUndefined : kotlinx/serialization/cbor/CborPrimitive { public static final field Companion Lkotlinx/serialization/cbor/CborUndefined$Companion; public synthetic fun ([JLkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun getValue ()Ljava/lang/Object; - public fun getValue ()Lkotlin/Unit; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class kotlinx/serialization/cbor/CborUndefined$Companion { diff --git a/formats/cbor/api/kotlinx-serialization-cbor.klib.api b/formats/cbor/api/kotlinx-serialization-cbor.klib.api index 0ce3d8de4d..7e3dc118ae 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.klib.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.klib.api @@ -95,6 +95,10 @@ final class kotlinx.serialization.cbor/CborBoolean : kotlinx.serialization.cbor/ final val value // kotlinx.serialization.cbor/CborBoolean.value|{}value[0] final fun (): kotlin/Boolean // kotlinx.serialization.cbor/CborBoolean.value.|(){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborBoolean.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborBoolean.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // kotlinx.serialization.cbor/CborBoolean.toString|toString(){}[0] + final object Companion { // kotlinx.serialization.cbor/CborBoolean.Companion|null[0] final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborBoolean.Companion.serializer|serializer(){}[0] } @@ -142,11 +146,9 @@ final class kotlinx.serialization.cbor/CborBuilder { // kotlinx.serialization.cb final class kotlinx.serialization.cbor/CborByteString : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborByteString|null[0] constructor (kotlin/ByteArray, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborByteString.|(kotlin.ByteArray;kotlin.ULongArray...){}[0] - final val value // kotlinx.serialization.cbor/CborByteString.value|{}value[0] - final fun (): kotlin/ByteArray // kotlinx.serialization.cbor/CborByteString.value.|(){}[0] - final fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborByteString.equals|equals(kotlin.Any?){}[0] final fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborByteString.hashCode|hashCode(){}[0] + final fun toByteArray(): kotlin/ByteArray // kotlinx.serialization.cbor/CborByteString.toByteArray|toByteArray(){}[0] final fun toString(): kotlin/String // kotlinx.serialization.cbor/CborByteString.toString|toString(){}[0] final object Companion { // kotlinx.serialization.cbor/CborByteString.Companion|null[0] @@ -187,6 +189,10 @@ final class kotlinx.serialization.cbor/CborFloat : kotlinx.serialization.cbor/Cb final val value // kotlinx.serialization.cbor/CborFloat.value|{}value[0] final fun (): kotlin/Double // kotlinx.serialization.cbor/CborFloat.value.|(){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborFloat.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborFloat.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // kotlinx.serialization.cbor/CborFloat.toString|toString(){}[0] + final object Companion { // kotlinx.serialization.cbor/CborFloat.Companion|null[0] final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborFloat.Companion.serializer|serializer(){}[0] } @@ -195,10 +201,10 @@ final class kotlinx.serialization.cbor/CborFloat : kotlinx.serialization.cbor/Cb final class kotlinx.serialization.cbor/CborInteger : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborInteger|null[0] constructor (kotlin/ULong, kotlin/Boolean, kotlin/ULongArray...) // kotlinx.serialization.cbor/CborInteger.|(kotlin.ULong;kotlin.Boolean;kotlin.ULongArray...){}[0] + final val absoluteValue // kotlinx.serialization.cbor/CborInteger.absoluteValue|{}absoluteValue[0] + final fun (): kotlin/ULong // kotlinx.serialization.cbor/CborInteger.absoluteValue.|(){}[0] final val isPositive // kotlinx.serialization.cbor/CborInteger.isPositive|{}isPositive[0] final fun (): kotlin/Boolean // kotlinx.serialization.cbor/CborInteger.isPositive.|(){}[0] - final val value // kotlinx.serialization.cbor/CborInteger.value|{}value[0] - final fun (): kotlin/ULong // kotlinx.serialization.cbor/CborInteger.value.|(){}[0] final fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborInteger.equals|equals(kotlin.Any?){}[0] final fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborInteger.hashCode|hashCode(){}[0] @@ -246,8 +252,9 @@ final class kotlinx.serialization.cbor/CborMap : kotlin.collections/Map(kotlin/ULongArray...) // kotlinx.serialization.cbor/CborNull.|(kotlin.ULongArray...){}[0] - final val value // kotlinx.serialization.cbor/CborNull.value|{}value[0] - final fun () // kotlinx.serialization.cbor/CborNull.value.|(){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborNull.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborNull.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // kotlinx.serialization.cbor/CborNull.toString|toString(){}[0] final object Companion { // kotlinx.serialization.cbor/CborNull.Companion|null[0] final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborNull.Companion.serializer|serializer(){}[0] @@ -260,6 +267,10 @@ final class kotlinx.serialization.cbor/CborString : kotlinx.serialization.cbor/C final val value // kotlinx.serialization.cbor/CborString.value|{}value[0] final fun (): kotlin/String // kotlinx.serialization.cbor/CborString.value.|(){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborString.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborString.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // kotlinx.serialization.cbor/CborString.toString|toString(){}[0] + final object Companion { // kotlinx.serialization.cbor/CborString.Companion|null[0] final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborString.Companion.serializer|serializer(){}[0] } @@ -268,8 +279,9 @@ final class kotlinx.serialization.cbor/CborString : kotlinx.serialization.cbor/C final class kotlinx.serialization.cbor/CborUndefined : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborUndefined|null[0] constructor (kotlin/ULongArray...) // kotlinx.serialization.cbor/CborUndefined.|(kotlin.ULongArray...){}[0] - final val value // kotlinx.serialization.cbor/CborUndefined.value|{}value[0] - final fun () // kotlinx.serialization.cbor/CborUndefined.value.|(){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborUndefined.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborUndefined.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // kotlinx.serialization.cbor/CborUndefined.toString|toString(){}[0] final object Companion { // kotlinx.serialization.cbor/CborUndefined.Companion|null[0] final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborUndefined.Companion.serializer|serializer(){}[0] @@ -306,13 +318,6 @@ sealed class kotlinx.serialization.cbor/CborElement { // kotlinx.serialization.c } sealed class kotlinx.serialization.cbor/CborPrimitive : kotlinx.serialization.cbor/CborElement { // kotlinx.serialization.cbor/CborPrimitive|null[0] - abstract val value // kotlinx.serialization.cbor/CborPrimitive.value|{}value[0] - abstract fun (): kotlin/Any // kotlinx.serialization.cbor/CborPrimitive.value.|(){}[0] - - open fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.serialization.cbor/CborPrimitive.equals|equals(kotlin.Any?){}[0] - open fun hashCode(): kotlin/Int // kotlinx.serialization.cbor/CborPrimitive.hashCode|hashCode(){}[0] - open fun toString(): kotlin/String // kotlinx.serialization.cbor/CborPrimitive.toString|toString(){}[0] - final object Companion { // kotlinx.serialization.cbor/CborPrimitive.Companion|null[0] final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborPrimitive.Companion.serializer|serializer(){}[0] } diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt index 91b91a1213..974ced5516 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -63,32 +63,7 @@ public sealed class CborElement( @ExperimentalSerializationApi public sealed class CborPrimitive( tags: ULongArray = EMPTY_TAGS -) : CborElement(tags) { - protected abstract val value: Any - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is CborPrimitive) return false - if (!super.equals(other)) return false - - if (value != other.value) return false - - return true - } - - override fun hashCode(): Int { - var result = super.hashCode() - result = 31 * result + value.hashCode() - return result - } - - override fun toString(): String { - return "${this::class.simpleName}(" + - "tags=${tags.joinToString(prefix = "[", postfix = "]")}, " + - "value=$value" + - ")" - } -} +) : CborElement(tags) /** * Class representing either: @@ -100,12 +75,10 @@ public sealed class CborPrimitive( @Serializable(with = CborIntSerializer::class) @ExperimentalSerializationApi public class CborInteger( - absoluteValue: ULong, + public val absoluteValue: ULong, public val isPositive: Boolean, vararg tags: ULong ) : CborPrimitive(tags) { - public override val value: ULong = absoluteValue - init { if (!isPositive) require(absoluteValue > 0uL) { "Illegal absolute value $absoluteValue for a negative number." } } @@ -115,6 +88,7 @@ public class CborInteger( if (other !is CborInteger) return false if (!super.equals(other)) return false + if (absoluteValue != other.absoluteValue) return false if (isPositive != other.isPositive) return false return true @@ -122,17 +96,18 @@ public class CborInteger( override fun hashCode(): Int { var result = super.hashCode() + result = 31 * result + absoluteValue.hashCode() result = 31 * result + isPositive.hashCode() return result } override fun toString(): String { return "CborInt(tags=${tags.joinToString(prefix = "[", postfix = "]")}, " + - "value=" + when (isPositive) { + "absoluteValue=" + when (isPositive) { true -> "" false -> "-" } + - value + + absoluteValue + ")" } } @@ -174,11 +149,11 @@ public val CborInteger.longOrNull: Long? get() { val max = Long.MAX_VALUE.toULong() return if (isPositive) { - if (value <= max) value.toLong() else null + if (absoluteValue <= max) absoluteValue.toLong() else null } else { when { - value <= max -> -value.toLong() - value == max + 1uL -> Long.MIN_VALUE + absoluteValue <= max -> -absoluteValue.toLong() + absoluteValue == max + 1uL -> Long.MIN_VALUE else -> null } } @@ -244,9 +219,25 @@ public val CborInteger.byteOrNull: Byte? @Serializable(with = CborFloatSerializer::class) @ExperimentalSerializationApi public class CborFloat( - public override val value: Double, + public val value: Double, vararg tags: ULong -) : CborPrimitive(tags) +) : CborPrimitive(tags) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CborFloat) return false + if (!super.equals(other)) return false + return value.equals(other.value) + } + + override fun hashCode(): Int = 31 * super.hashCode() + value.hashCode() + + override fun toString(): String { + return "CborFloat(" + + "tags=${tags.joinToString(prefix = "[", postfix = "]")}, " + + "value=$value" + + ")" + } +} /** * Class representing CBOR string value. @@ -254,9 +245,25 @@ public class CborFloat( @Serializable(with = CborStringSerializer::class) @ExperimentalSerializationApi public class CborString( - public override val value: String, + public val value: String, vararg tags: ULong -) : CborPrimitive(tags) +) : CborPrimitive(tags) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CborString) return false + if (!super.equals(other)) return false + return value == other.value + } + + override fun hashCode(): Int = 31 * super.hashCode() + value.hashCode() + + override fun toString(): String { + return "CborString(" + + "tags=${tags.joinToString(prefix = "[", postfix = "]")}, " + + "value=$value" + + ")" + } +} /** * Class representing CBOR boolean value. @@ -264,9 +271,25 @@ public class CborString( @Serializable(with = CborBooleanSerializer::class) @ExperimentalSerializationApi public class CborBoolean( - public override val value: Boolean, + public val value: Boolean, vararg tags: ULong -) : CborPrimitive(tags) +) : CborPrimitive(tags) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CborBoolean) return false + if (!super.equals(other)) return false + return value == other.value + } + + override fun hashCode(): Int = 31 * super.hashCode() + value.hashCode() + + override fun toString(): String { + return "CborBoolean(" + + "tags=${tags.joinToString(prefix = "[", postfix = "]")}, " + + "value=$value" + + ")" + } +} /** * Class representing CBOR byte string value. @@ -274,28 +297,35 @@ public class CborBoolean( @Serializable(with = CborByteStringSerializer::class) @ExperimentalSerializationApi public class CborByteString( - public override val value: ByteArray, + private val bytes: ByteArray, vararg tags: ULong ) : CborPrimitive(tags) { + /** + * Returns a copy of this CBOR byte string contents. + */ + public fun toByteArray(): ByteArray = bytes.copyOf() + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is CborByteString) return false if (!tags.contentEquals(other.tags)) return false - return value.contentEquals(other.value) + return bytes.contentEquals(other.bytes) } override fun hashCode(): Int { var result = tags.contentHashCode() - result = 31 * result + (value.contentHashCode()) + result = 31 * result + (bytes.contentHashCode()) return result } override fun toString(): String { return "CborByteString(" + "tags=${tags.joinToString(prefix = "[", postfix = "]")}, " + - "value=h'${value.toHexString()}" + + "bytes=h'${bytes.toHexString()}" + ")" } + + internal fun getBytes(): ByteArray = bytes } /** @@ -304,7 +334,17 @@ public class CborByteString( @Serializable(with = CborNullSerializer::class) @ExperimentalSerializationApi public class CborNull(vararg tags: ULong) : CborPrimitive(tags) { - public override val value: Unit = Unit + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CborNull) return false + return super.equals(other) + } + + override fun hashCode(): Int = CborNull::class.hashCode() * 31 + super.hashCode() + + override fun toString(): String { + return "CborNull(tags=${tags.joinToString(prefix = "[", postfix = "]")})" + } } /** @@ -313,7 +353,17 @@ public class CborNull(vararg tags: ULong) : CborPrimitive(tags) { @Serializable(with = CborUndefinedSerializer::class) @ExperimentalSerializationApi public class CborUndefined(vararg tags: ULong) : CborPrimitive(tags) { - public override val value: Unit = Unit + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CborUndefined) return false + return super.equals(other) + } + + override fun hashCode(): Int = CborUndefined::class.hashCode() * 31 + super.hashCode() + + override fun toString(): String { + return "CborUndefined(tags=${tags.joinToString(prefix = "[", postfix = "]")})" + } } /** diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index d161b996b0..2e57f2330e 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -123,8 +123,8 @@ internal object CborIntSerializer : KSerializer, CborSerializer { cborEncoder.encodeTags(value.tags) when (value.isPositive) { //@formatter:off - true -> cborEncoder.encodePositive(value.value) - false -> cborEncoder.encodeNegative(value.value) + true -> cborEncoder.encodePositive(value.absoluteValue) + false -> cborEncoder.encodeNegative(value.absoluteValue) //@formatter:on } } @@ -208,7 +208,7 @@ internal object CborByteStringSerializer : KSerializer, CborSeri override fun serialize(encoder: Encoder, value: CborByteString) { val cborEncoder = encoder.asCborEncoder() cborEncoder.encodeTags(value.tags) - cborEncoder.encodeByteString(value.value) + cborEncoder.encodeByteString(value.getBytes()) } override fun deserialize(decoder: Decoder): CborByteString { 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 e75a77ffea..8dc0fa6a5c 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -812,7 +812,7 @@ internal class StructuredCborParser(internal val element: CborElement, private v if (layer.current !is CborByteString) { throw CborDecodingException("Expected byte string, got ${layer.current::class.simpleName}") } - return (layer.current as CborByteString).value + return (layer.current as CborByteString).toByteArray() //do we want to copy here? } override fun nextDouble(tags: ULongArray?): Double { diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Unsigned.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Unsigned.kt index 210148ea24..2a14dc0914 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Unsigned.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Unsigned.kt @@ -72,7 +72,7 @@ internal class UnsignedInlineDecoder( if (!integer.isPositive) { throw CborDecodingException("Expected unsigned integer, got $integer") } - integer.value.toLong() + integer.absoluteValue.toLong() } is CborParser -> { diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDelegatedPropertiesTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDelegatedPropertiesTest.kt index 8c3265ffd1..204a14fdb1 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDelegatedPropertiesTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDelegatedPropertiesTest.kt @@ -74,9 +74,9 @@ class CborDelegatedPropertiesTest { for ((key, value) in map.entries) { val mappedKey = when (key) { is CborInteger -> when { - key.isPositive && key.value == NAME_LABEL.toULong() -> CborString(NAME_SPEC.name, *NAME_SPEC.keyTags) - key.isPositive && key.value == AGE_LABEL.toULong() -> CborString(AGE_SPEC.name, *AGE_SPEC.keyTags) - key.isPositive && key.value == GENDER_LABEL.toULong() -> CborString(GENDER_SPEC.name, *GENDER_SPEC.keyTags) + key.isPositive && key.absoluteValue == NAME_LABEL.toULong() -> CborString(NAME_SPEC.name, *NAME_SPEC.keyTags) + key.isPositive && key.absoluteValue == AGE_LABEL.toULong() -> CborString(AGE_SPEC.name, *AGE_SPEC.keyTags) + key.isPositive && key.absoluteValue == GENDER_LABEL.toULong() -> CborString(GENDER_SPEC.name, *GENDER_SPEC.keyTags) else -> key } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index 70964a019d..fd06e5c461 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -22,7 +22,7 @@ class CborElementTest { val configured = Cbor { alwaysUseByteString = true } val element = configured.encodeToCborElement(byteArrayOf(1, 2, 3)) assertTrue(element is CborByteString) - assertTrue(element.value.contentEquals(byteArrayOf(1, 2, 3))) + assertTrue(element.toByteArray().contentEquals(byteArrayOf(1, 2, 3))) assertTrue(configured.decodeFromCborElement(element).contentEquals(byteArrayOf(1, 2, 3))) } @@ -63,7 +63,7 @@ class CborElementTest { val numberBytes = cbor.encodeToByteArray(numberElement) val decodedNumber = cbor.decodeFromByteArray(numberBytes) assertEquals(numberElement, decodedNumber) - assertEquals(42u, (decodedNumber as CborInteger).value) + assertEquals(42uL, (decodedNumber as CborInteger).absoluteValue) } @Test @@ -71,33 +71,33 @@ class CborElementTest { val numberElement = CborInteger(0uL) assertEquals(numberElement, CborInteger(0)) assertEquals(numberElement.isPositive, true) - assertEquals(numberElement.value, 0uL) + assertEquals(numberElement.absoluteValue, 0uL) val numberBytes = cbor.encodeToByteArray(numberElement) val decodedNumber = cbor.decodeFromByteArray(numberBytes) assertEquals(numberElement, decodedNumber) - assertEquals(0uL, (decodedNumber as CborInteger).value) + assertEquals(0uL, (decodedNumber as CborInteger).absoluteValue) } @Test fun testCborNumberMax() { val numberElement = CborInteger(ULong.MAX_VALUE) assertEquals(numberElement.isPositive, true) - assertEquals(numberElement.value, ULong.MAX_VALUE) + assertEquals(numberElement.absoluteValue, ULong.MAX_VALUE) val numberBytes = cbor.encodeToByteArray(numberElement) val decodedNumber = cbor.decodeFromByteArray(numberBytes) assertEquals(numberElement, decodedNumber) - assertEquals(ULong.MAX_VALUE, (decodedNumber as CborInteger).value) + assertEquals(ULong.MAX_VALUE, (decodedNumber as CborInteger).absoluteValue) } @Test fun testCborNumberMaxHalv() { val numberElement = CborInteger(Long.MAX_VALUE) assertEquals(numberElement.isPositive, true) - assertEquals(numberElement.value, Long.MAX_VALUE.toULong()) + assertEquals(numberElement.absoluteValue, Long.MAX_VALUE.toULong()) val numberBytes = cbor.encodeToByteArray(numberElement) val decodedNumber = cbor.decodeFromByteArray(numberBytes) assertEquals(numberElement, decodedNumber) - assertEquals(Long.MAX_VALUE.toULong(), (decodedNumber as CborInteger).value) + assertEquals(Long.MAX_VALUE.toULong(), (decodedNumber as CborInteger).absoluteValue) } @@ -105,11 +105,11 @@ class CborElementTest { fun testCborNumberMin() { val numberElement = CborInteger(ULong.MAX_VALUE, isPositive = false) assertEquals(numberElement.isPositive, false) - assertEquals(numberElement.value, ULong.MAX_VALUE) + assertEquals(numberElement.absoluteValue, ULong.MAX_VALUE) val numberBytes = cbor.encodeToByteArray(numberElement) val decodedNumber = cbor.decodeFromByteArray(numberBytes) assertEquals(numberElement, decodedNumber) - assertEquals(ULong.MAX_VALUE, (decodedNumber as CborInteger).value) + assertEquals(ULong.MAX_VALUE, (decodedNumber as CborInteger).absoluteValue) assertNull(numberElement.longOrNull) assertFailsWith { numberElement.long } @@ -121,11 +121,11 @@ class CborElementTest { fun testCborNumberMinHalv() { val numberElement = CborInteger(Long.MAX_VALUE.toULong(), isPositive = false) assertEquals(numberElement.isPositive, false) - assertEquals(numberElement.value, Long.MAX_VALUE.toULong()) + assertEquals(numberElement.absoluteValue, Long.MAX_VALUE.toULong()) val numberBytes = cbor.encodeToByteArray(numberElement) val decodedNumber = cbor.decodeFromByteArray(numberBytes) assertEquals(numberElement, decodedNumber) - assertEquals(Long.MAX_VALUE.toULong(), (decodedNumber as CborInteger).value) + assertEquals(Long.MAX_VALUE.toULong(), (decodedNumber as CborInteger).absoluteValue) val long = cbor.decodeFromCborElement(numberElement) @@ -166,7 +166,7 @@ class CborElementTest { val byteStringBytes = cbor.encodeToByteArray(byteStringElement) val decodedByteString = cbor.decodeFromByteArray(byteStringBytes) assertEquals(byteStringElement, decodedByteString) - assertTrue((decodedByteString as CborByteString).value.contentEquals(byteArray)) + assertTrue((decodedByteString as CborByteString).toByteArray().contentEquals(byteArray)) } @Test @@ -188,7 +188,7 @@ class CborElementTest { // Verify individual elements assertTrue(decodedList[0] is CborInteger) - assertEquals(1u, (decodedList[0] as CborInteger).value) + assertEquals(1uL, (decodedList[0] as CborInteger).absoluteValue) assertTrue(decodedList[1] is CborString) assertEquals("two", (decodedList[1] as CborString).value) @@ -225,7 +225,7 @@ class CborElementTest { assertTrue(decodedMap.containsKey(CborString("key1"))) val value1 = decodedMap[CborString("key1")] assertTrue(value1 is CborInteger) - assertEquals(42u, (value1 as CborInteger).value) + assertEquals(42uL, (value1 as CborInteger).absoluteValue) assertTrue(decodedMap.containsKey(CborString("key2"))) val value2 = decodedMap[CborString("key2")] @@ -284,7 +284,7 @@ class CborElementTest { assertEquals(5, primitivesValue.size) assertTrue(primitivesValue[0] is CborInteger) - assertEquals(123u, (primitivesValue[0] as CborInteger).value) + assertEquals(123uL, (primitivesValue[0] as CborInteger).absoluteValue) assertTrue(primitivesValue[1] is CborString) assertEquals("text", (primitivesValue[1] as CborString).value) @@ -293,7 +293,7 @@ class CborElementTest { assertEquals(false, (primitivesValue[2] as CborBoolean).value) assertTrue(primitivesValue[3] is CborByteString) - assertTrue((primitivesValue[3] as CborByteString).value.contentEquals(byteArrayOf(10, 20, 30))) + assertTrue((primitivesValue[3] as CborByteString).toByteArray().contentEquals(byteArrayOf(10, 20, 30))) assertTrue(primitivesValue[4] is CborNull) @@ -312,10 +312,10 @@ class CborElementTest { assertEquals(2, innerValue.size) assertTrue(innerValue[0] is CborInteger) - assertEquals(1u, (innerValue[0] as CborInteger).value) + assertEquals(1uL, (innerValue[0] as CborInteger).absoluteValue) assertTrue(innerValue[1] is CborInteger) - assertEquals(2u, (innerValue[1] as CborInteger).value) + assertEquals(2uL, (innerValue[1] as CborInteger).absoluteValue) // Verify the empty list assertTrue(nestedValue.containsKey(CborString("empty"))) @@ -330,7 +330,7 @@ class CborElementTest { fun testDecodePositiveInt() { // Test data from CborParserTest.testParseIntegers val element = cbor.decodeFromHexString("0C") as CborInteger - assertEquals(12u, element.value) + assertEquals(12uL, element.absoluteValue) } @Test @@ -365,7 +365,7 @@ class CborElementTest { assertTrue(element is CborByteString) val byteString = element as CborByteString val expectedBytes = HexConverter.parseHexBinary("aabbccddeeff99") - assertTrue(byteString.value.contentEquals(expectedBytes)) + assertTrue(byteString.toByteArray().contentEquals(expectedBytes)) } @Test @@ -375,9 +375,9 @@ class CborElementTest { assertTrue(element is CborArray) val list = element as CborArray assertEquals(3, list.size) - assertEquals(1u, (list[0] as CborInteger).value) - assertEquals(255u, (list[1] as CborInteger).value) - assertEquals(65536u, (list[2] as CborInteger).value) + assertEquals(1uL, (list[0] as CborInteger).absoluteValue) + assertEquals(255uL, (list[1] as CborInteger).absoluteValue) + assertEquals(65536uL, (list[2] as CborInteger).absoluteValue) } @Test @@ -403,7 +403,7 @@ class CborElementTest { // Check the byte string val byteString = map[CborString("a")] as CborByteString val expectedBytes = HexConverter.parseHexBinary("cafe010203") - assertTrue(byteString.value.contentEquals(expectedBytes)) + assertTrue(byteString.toByteArray().contentEquals(expectedBytes)) // Check the text string assertEquals(CborString("Hello world"), map[CborString("b")]) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborUnsignedInteroperabilityTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborUnsignedInteroperabilityTest.kt index e445c93814..8fed710fb3 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborUnsignedInteroperabilityTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborUnsignedInteroperabilityTest.kt @@ -15,7 +15,7 @@ class CborUnsignedInteroperabilityTest { val element = Cbor.decodeFromHexString(canonicalUnsigned200) val asInteger = element as? CborInteger ?: fail("Expected CborInteger, got ${element::class}") assertTrue(asInteger.isPositive) - assertEquals(200uL, asInteger.value) + assertEquals(200uL, asInteger.absoluteValue) assertEquals(200u.toUByte(), Cbor.decodeFromHexString(canonicalUnsigned200)) assertEquals(canonicalUnsigned200, Cbor.encodeToHexString(200u.toUByte())) From aef37f7a27ffa1dbdc1914f5f87cf456d9d127b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Tue, 19 May 2026 19:27:51 +0200 Subject: [PATCH 64/68] CBOR: rearrange internal CBOR writer apis --- .../cbor/api/kotlinx-serialization-cbor.api | 5 -- .../api/kotlinx-serialization-cbor.klib.api | 5 -- .../kotlinx/serialization/cbor/CborEncoder.kt | 28 ------- .../cbor/internal/CborElementSerializers.kt | 52 ++++++------- .../cbor/internal/CborWriterInterface.kt | 27 +++++++ .../serialization/cbor/internal/Encoder.kt | 5 +- .../serialization/cbor/CborElementTest.kt | 18 +++++ .../CborEncoderEncodeTagsValidationTest.kt | 74 ------------------- guide/test/FormatsTest.kt | 2 +- 9 files changed, 76 insertions(+), 140 deletions(-) create mode 100644 formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborWriterInterface.kt delete mode 100644 formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborEncoderEncodeTagsValidationTest.kt diff --git a/formats/cbor/api/kotlinx-serialization-cbor.api b/formats/cbor/api/kotlinx-serialization-cbor.api index 564aadb8eb..56d640e541 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.api @@ -171,12 +171,7 @@ public final class kotlinx/serialization/cbor/CborElementKt { } public abstract interface class kotlinx/serialization/cbor/CborEncoder : kotlinx/serialization/encoding/Encoder { - public abstract fun encodeByteString ([B)V public fun encodeCborElement (Lkotlinx/serialization/cbor/CborElement;)V - public abstract fun encodeNegative-VKZWuLQ (J)V - public abstract fun encodePositive-VKZWuLQ (J)V - public abstract fun encodeTags-QwZRm1k ([J)V - public abstract fun encodeUndefined ()V public abstract fun getCbor ()Lkotlinx/serialization/cbor/Cbor; } diff --git a/formats/cbor/api/kotlinx-serialization-cbor.klib.api b/formats/cbor/api/kotlinx-serialization-cbor.klib.api index 7e3dc118ae..9e98d3e192 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.klib.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.klib.api @@ -53,11 +53,6 @@ abstract interface kotlinx.serialization.cbor/CborEncoder : kotlinx.serializatio abstract val cbor // kotlinx.serialization.cbor/CborEncoder.cbor|{}cbor[0] abstract fun (): kotlinx.serialization.cbor/Cbor // kotlinx.serialization.cbor/CborEncoder.cbor.|(){}[0] - abstract fun encodeByteString(kotlin/ByteArray) // kotlinx.serialization.cbor/CborEncoder.encodeByteString|encodeByteString(kotlin.ByteArray){}[0] - abstract fun encodeNegative(kotlin/ULong) // kotlinx.serialization.cbor/CborEncoder.encodeNegative|encodeNegative(kotlin.ULong){}[0] - abstract fun encodePositive(kotlin/ULong) // kotlinx.serialization.cbor/CborEncoder.encodePositive|encodePositive(kotlin.ULong){}[0] - abstract fun encodeTags(kotlin/ULongArray) // kotlinx.serialization.cbor/CborEncoder.encodeTags|encodeTags(kotlin.ULongArray){}[0] - abstract fun encodeUndefined() // kotlinx.serialization.cbor/CborEncoder.encodeUndefined|encodeUndefined(){}[0] open fun encodeCborElement(kotlinx.serialization.cbor/CborElement) // kotlinx.serialization.cbor/CborEncoder.encodeCborElement|encodeCborElement(kotlinx.serialization.cbor.CborElement){}[0] } diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt index 1f181d3717..c9a5f97172 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt @@ -62,32 +62,4 @@ public interface CborEncoder : Encoder { */ public fun encodeCborElement(element: CborElement): Unit = encodeSerializableValue(CborElementSerializer, element) - /** - * Allows manually encoding CBOR tags. Use with caution, as it is possible to produce invalid CBOR if invoked carelessly! - */ - public fun encodeTags(@OptIn(kotlin.ExperimentalUnsignedTypes::class) tags: ULongArray): Unit - - /** - * Encode a CBOR byte string (major type 2). - * - * This exists for low-level CBOR serializers and for encoding [CborByteString] values. - */ - public fun encodeByteString(byteArray: ByteArray) - - /** - * Encode CBOR `undefined` (simple value 23 / `0xF7`). - */ - public fun encodeUndefined() - - /** - * Encode a negative integer (major type 1) with an absolute value that may exceed [Long.MIN_VALUE]. - * - * The encoded CBOR value is `-1 - (value - 1)` (i.e. `-value` in terms of [CborInteger]'s absolute representation). - */ - public fun encodeNegative(value: ULong) - - /** - * Encode an unsigned integer (major type 0) that may exceed [Long.MAX_VALUE]. - */ - public fun encodePositive(value: ULong) } diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index 2e57f2330e..a8339b8701 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -31,7 +31,7 @@ internal object CborElementSerializer : KSerializer, CborSerializer } override fun serialize(encoder: Encoder, value: CborElement) { - encoder.asCborEncoder() + encoder.asCborWriter() // Encode the value when (value) { @@ -84,8 +84,8 @@ internal object CborNullSerializer : KSerializer, CborSerializer { buildSerialDescriptor("kotlinx.serialization.cbor.CborNull", SerialKind.ENUM) override fun serialize(encoder: Encoder, value: CborNull) { - val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value.tags) + val cborWriter = encoder.asCborWriter() + cborWriter.encodeTags(value.tags) encoder.encodeNull() } @@ -101,9 +101,9 @@ internal object CborUndefinedSerializer : KSerializer, CborSerial buildSerialDescriptor("kotlinx.serialization.cbor.CborUndefined", SerialKind.ENUM) override fun serialize(encoder: Encoder, value: CborUndefined) { - val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value.tags) - cborEncoder.encodeUndefined() + val cborWriter = encoder.asCborWriter() + cborWriter.encodeTags(value.tags) + cborWriter.encodeUndefined() } override fun deserialize(decoder: Decoder): CborUndefined { @@ -119,12 +119,12 @@ internal object CborIntSerializer : KSerializer, CborSerializer { PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborInt", PrimitiveKind.LONG) override fun serialize(encoder: Encoder, value: CborInteger) { - val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value.tags) + val cborWriter = encoder.asCborWriter() + cborWriter.encodeTags(value.tags) when (value.isPositive) { //@formatter:off - true -> cborEncoder.encodePositive(value.absoluteValue) - false -> cborEncoder.encodeNegative(value.absoluteValue) + true -> cborWriter.encodePositive(value.absoluteValue) + false -> cborWriter.encodeNegative(value.absoluteValue) //@formatter:on } } @@ -141,8 +141,8 @@ internal object CborFloatSerializer : KSerializer, CborSerializer { PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborDouble", PrimitiveKind.DOUBLE) override fun serialize(encoder: Encoder, value: CborFloat) { - val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value.tags) + val cborWriter = encoder.asCborWriter() + cborWriter.encodeTags(value.tags) encoder.encodeDouble(value.value) } @@ -162,8 +162,8 @@ internal object CborStringSerializer : KSerializer, CborSerializer { PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborString", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: CborString) { - val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value.tags) + val cborWriter = encoder.asCborWriter() + cborWriter.encodeTags(value.tags) encoder.encodeString(value.value) } @@ -184,8 +184,8 @@ internal object CborBooleanSerializer : KSerializer, CborSerializer PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborBoolean", PrimitiveKind.BOOLEAN) override fun serialize(encoder: Encoder, value: CborBoolean) { - val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value.tags) + val cborWriter = encoder.asCborWriter() + cborWriter.encodeTags(value.tags) encoder.encodeBoolean(value.value) } @@ -206,9 +206,9 @@ internal object CborByteStringSerializer : KSerializer, CborSeri SerialDescriptor("kotlinx.serialization.cbor.CborByteString", ByteArraySerializer().descriptor) override fun serialize(encoder: Encoder, value: CborByteString) { - val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value.tags) - cborEncoder.encodeByteString(value.getBytes()) + val cborWriter = encoder.asCborWriter() + cborWriter.encodeTags(value.tags) + cborWriter.encodeByteString(value.getBytes()) } override fun deserialize(decoder: Decoder): CborByteString { @@ -231,8 +231,8 @@ internal object CborMapSerializer : KSerializer, CborSerializer { ) override fun serialize(encoder: Encoder, value: CborMap) { - val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value.tags) + val cborWriter = encoder.asCborWriter() + cborWriter.encodeTags(value.tags) MapSerializer(CborElementSerializer, CborElementSerializer).serialize(encoder, value) } @@ -255,8 +255,8 @@ internal object CborArraySerializer : KSerializer, CborSerializer { ) override fun serialize(encoder: Encoder, value: CborArray) { - val cborEncoder = encoder.asCborEncoder() - cborEncoder.encodeTags(value.tags) + val cborWriter = encoder.asCborWriter() + cborWriter.encodeTags(value.tags) ListSerializer(CborElementSerializer).serialize(encoder, value) } @@ -274,12 +274,12 @@ internal fun Decoder.asCborDecoder(): CborDecoder = this as? CborDecoder "Expected Decoder to be CborDecoder, got ${this::class}" ) -/*need to expose writer to access encodeTag()*/ +/*need to expose writer to access raw CBOR token operations*/ @IgnorableReturnValue -internal fun Encoder.asCborEncoder() = this as? CborEncoder +internal fun Encoder.asCborWriter() = this as? CborWriterInterface ?: throw IllegalStateException( "This serializer can be used only with Cbor format. " + - "Expected Encoder to be CborEncoder, got ${this::class}" + "Expected Encoder to provide a CborWriterInterface, got ${this::class}" ) /** diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborWriterInterface.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborWriterInterface.kt new file mode 100644 index 0000000000..4f51ab3772 --- /dev/null +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborWriterInterface.kt @@ -0,0 +1,27 @@ +@file:OptIn(ExperimentalSerializationApi::class, ExperimentalUnsignedTypes::class) + +package kotlinx.serialization.cbor.internal + +import kotlinx.serialization.* + +/** + * Common interface for CBOR writers that can emit CBOR data to different destinations. + */ +internal sealed interface CborWriterInterface { + // Collection operations are represented by Encoder.beginStructure/endStructure. + + // Value writing operations + fun encodeNull() + fun encodeBoolean(value: Boolean) + fun encodeRawNumber(value: Long) + fun encodePositive(value: ULong) + fun encodeNegative(value: ULong) + fun encodeString(value: String) + fun encodeByteString(byteArray: ByteArray) + fun encodeDouble(value: Double) + fun encodeFloat(value: Float) + fun encodeUndefined() + + // Tag writing + fun encodeTags(tags: ULongArray) +} 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 aad225a2d7..d112d6b868 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -26,7 +26,7 @@ private fun Stack.peek() = last() // Split implementation to optimize base case internal sealed class CborWriter( override val cbor: Cbor, -) : AbstractEncoder(), CborEncoder { +) : AbstractEncoder(), CborEncoder, CborWriterInterface { private var tagsMustBeFollowedByDataItem: Boolean = false @@ -147,6 +147,9 @@ internal sealed class CborWriter( onDataItemEncoded() getDestination().encodeNumber(value) } + + final override fun encodeRawNumber(value: Long) = encodeLong(value) + override fun encodeNegative(value: ULong) { onDataItemEncoded() getDestination().encodeNegative(value) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index fd06e5c461..5af4287d28 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -735,6 +735,24 @@ class CborElementTest { assertEquals(element, cbor.decodeFromByteArray(bytes)) } + @OptIn(ExperimentalSerializationApi::class, ExperimentalUnsignedTypes::class) + @Test + fun testEncodeCborElementWritesTaggedElements() { + val element = CborMap( + mapOf( + CborString("undefined") to CborUndefined(1uL), + CborString("positive") to CborInteger(ULong.MAX_VALUE, isPositive = true, tags = ulongArrayOf(2uL)), + CborString("negative") to CborInteger(ULong.MAX_VALUE, isPositive = false, tags = ulongArrayOf(3uL)), + CborString("bytes") to CborByteString(byteArrayOf(0xca.toByte(), 0xfe.toByte()), 4uL), + CborString("array") to CborArray(listOf(CborNull(5uL)), 6uL), + ), + 7uL + ) + + val bytes = cbor.encodeToByteArray(CborElement.serializer(), element) + assertEquals(element, cbor.decodeFromByteArray(CborElement.serializer(), bytes)) + } + @OptIn(ExperimentalUnsignedTypes::class) @Test fun testTagsPreservedWhenDecodingTypedElements() { diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborEncoderEncodeTagsValidationTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborEncoderEncodeTagsValidationTest.kt deleted file mode 100644 index efea2709af..0000000000 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborEncoderEncodeTagsValidationTest.kt +++ /dev/null @@ -1,74 +0,0 @@ -package kotlinx.serialization.cbor - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerializationException -import kotlinx.serialization.builtins.* -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildClassSerialDescriptor -import kotlinx.serialization.encoding.CompositeEncoder -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlin.test.Test -import kotlin.test.assertFailsWith - -@OptIn(ExperimentalSerializationApi::class, ExperimentalUnsignedTypes::class) -class CborEncoderEncodeTagsValidationTest { - - private object DanglingRootTagsSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("DanglingRootTags") - - override fun serialize(encoder: Encoder, value: Unit) { - (encoder as CborEncoder).encodeTags(ulongArrayOf(1u)) - } - - override fun deserialize(decoder: Decoder): Unit = Unit - } - - private object DanglingTagsInStructureSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("DanglingTagsInStructure") { - element("x", Int.serializer().descriptor) - } - - override fun serialize(encoder: Encoder, value: Unit) { - val composite = encoder.beginStructure(descriptor) - (composite as CborEncoder).encodeTags(ulongArrayOf(1u)) - composite.endStructure(descriptor) - } - - override fun deserialize(decoder: Decoder): Unit = Unit - } - - @Test - fun danglingEncodeTagsFailsForByteEncoding_indefinite() { - val cbor = Cbor { useDefiniteLengthEncoding = false } - assertFailsWith { - cbor.encodeToByteArray(DanglingRootTagsSerializer, Unit) - } - assertFailsWith { - cbor.encodeToByteArray(DanglingTagsInStructureSerializer, Unit) - } - } - - @Test - fun danglingEncodeTagsFailsForByteEncoding_definite() { - val cbor = Cbor { useDefiniteLengthEncoding = true } - assertFailsWith { - cbor.encodeToByteArray(DanglingRootTagsSerializer, Unit) - } - assertFailsWith { - cbor.encodeToByteArray(DanglingTagsInStructureSerializer, Unit) - } - } - - @Test - fun danglingEncodeTagsFailsForStructuredEncoding() { - val cbor = Cbor {} - assertFailsWith { - cbor.encodeToCborElement(DanglingRootTagsSerializer, Unit) - } - assertFailsWith { - cbor.encodeToCborElement(DanglingTagsInStructureSerializer, Unit) - } - } -} diff --git a/guide/test/FormatsTest.kt b/guide/test/FormatsTest.kt index e8c84c77ff..75bb6f63b3 100644 --- a/guide/test/FormatsTest.kt +++ b/guide/test/FormatsTest.kt @@ -31,7 +31,7 @@ class FormatsTest { @Test fun testExampleFormats03() { captureOutput("ExampleFormats03") { example.exampleFormats03.main() }.verifyOutputLines( - "CborMap(tags=[], content={CborString(tags=[], value=bytes)=CborByteString(tags=[], value=h'666f6f)})" + "CborMap(tags=[], content={CborString(tags=[], value=bytes)=CborByteString(tags=[], bytes=h'666f6f)})" ) } From d50bc011bb4d05971aadd1306da806d59c69709d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Wed, 20 May 2026 06:32:41 +0200 Subject: [PATCH 65/68] CBOR: guard raw elements from tag/label annotations also: respect CBOR config switches wrt. tags+labels --- .../cbor/internal/CborElementSerializers.kt | 18 +- .../cbor/internal/CborWriterInterface.kt | 1 + .../serialization/cbor/internal/Decoder.kt | 2 + .../serialization/cbor/internal/Encoder.kt | 24 + .../serialization/cbor/internal/Encoding.kt | 38 + .../serialization/cbor/CborElementTest.kt | 698 +++++++++++++++--- 6 files changed, 659 insertions(+), 122 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index a8339b8701..06875c7daf 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -85,7 +85,7 @@ internal object CborNullSerializer : KSerializer, CborSerializer { override fun serialize(encoder: Encoder, value: CborNull) { val cborWriter = encoder.asCborWriter() - cborWriter.encodeTags(value.tags) + cborWriter.encodeElementTags(value.tags) encoder.encodeNull() } @@ -102,7 +102,7 @@ internal object CborUndefinedSerializer : KSerializer, CborSerial override fun serialize(encoder: Encoder, value: CborUndefined) { val cborWriter = encoder.asCborWriter() - cborWriter.encodeTags(value.tags) + cborWriter.encodeElementTags(value.tags) cborWriter.encodeUndefined() } @@ -120,7 +120,7 @@ internal object CborIntSerializer : KSerializer, CborSerializer { override fun serialize(encoder: Encoder, value: CborInteger) { val cborWriter = encoder.asCborWriter() - cborWriter.encodeTags(value.tags) + cborWriter.encodeElementTags(value.tags) when (value.isPositive) { //@formatter:off true -> cborWriter.encodePositive(value.absoluteValue) @@ -142,7 +142,7 @@ internal object CborFloatSerializer : KSerializer, CborSerializer { override fun serialize(encoder: Encoder, value: CborFloat) { val cborWriter = encoder.asCborWriter() - cborWriter.encodeTags(value.tags) + cborWriter.encodeElementTags(value.tags) encoder.encodeDouble(value.value) } @@ -163,7 +163,7 @@ internal object CborStringSerializer : KSerializer, CborSerializer { override fun serialize(encoder: Encoder, value: CborString) { val cborWriter = encoder.asCborWriter() - cborWriter.encodeTags(value.tags) + cborWriter.encodeElementTags(value.tags) encoder.encodeString(value.value) } @@ -185,7 +185,7 @@ internal object CborBooleanSerializer : KSerializer, CborSerializer override fun serialize(encoder: Encoder, value: CborBoolean) { val cborWriter = encoder.asCborWriter() - cborWriter.encodeTags(value.tags) + cborWriter.encodeElementTags(value.tags) encoder.encodeBoolean(value.value) } @@ -207,7 +207,7 @@ internal object CborByteStringSerializer : KSerializer, CborSeri override fun serialize(encoder: Encoder, value: CborByteString) { val cborWriter = encoder.asCborWriter() - cborWriter.encodeTags(value.tags) + cborWriter.encodeElementTags(value.tags) cborWriter.encodeByteString(value.getBytes()) } @@ -232,7 +232,7 @@ internal object CborMapSerializer : KSerializer, CborSerializer { override fun serialize(encoder: Encoder, value: CborMap) { val cborWriter = encoder.asCborWriter() - cborWriter.encodeTags(value.tags) + cborWriter.encodeElementTags(value.tags) MapSerializer(CborElementSerializer, CborElementSerializer).serialize(encoder, value) } @@ -256,7 +256,7 @@ internal object CborArraySerializer : KSerializer, CborSerializer { override fun serialize(encoder: Encoder, value: CborArray) { val cborWriter = encoder.asCborWriter() - cborWriter.encodeTags(value.tags) + cborWriter.encodeElementTags(value.tags) ListSerializer(CborElementSerializer).serialize(encoder, value) } diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborWriterInterface.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborWriterInterface.kt index 4f51ab3772..059f15a3d9 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborWriterInterface.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborWriterInterface.kt @@ -24,4 +24,5 @@ internal sealed interface CborWriterInterface { // Tag writing fun encodeTags(tags: ULongArray) + fun encodeElementTags(tags: ULongArray) } 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 8dc0fa6a5c..f41b9ea437 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -84,6 +84,7 @@ internal open class CborReader(override val cbor: Cbor, internal val parser: Cbo if (index == CompositeDecoder.UNKNOWN_NAME) { parser.skipElement(tags) } else { + descriptor.throwIfCborElementHasIncompatibleAnnotations(index) verifyKeyTags(descriptor, index, tags) knownIndex = index break @@ -95,6 +96,7 @@ internal open class CborReader(override val cbor: Cbor, internal val parser: Cbo val (elemName, tags) = decodeElementNameWithTags(descriptor) readProperties++ descriptor.getElementIndexOrThrow(elemName).also { index -> + descriptor.throwIfCborElementHasIncompatibleAnnotations(index) verifyKeyTags(descriptor, index, tags) } } 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 d112d6b868..1c088acae2 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -22,6 +22,8 @@ private fun Stack.push(value: CborWriter.Data) = add(value) private fun Stack.pop() = removeLast() private fun Stack.peek() = last() +private enum class RawElementTagPosition { KEY, VALUE } + // Writes class as map [fieldName, fieldValue] // Split implementation to optimize base case internal sealed class CborWriter( @@ -29,6 +31,7 @@ internal sealed class CborWriter( ) : AbstractEncoder(), CborEncoder, CborWriterInterface { private var tagsMustBeFollowedByDataItem: Boolean = false + private var rawElementTagPosition: RawElementTagPosition = RawElementTagPosition.VALUE protected fun onDataItemEncoded() { tagsMustBeFollowedByDataItem = false @@ -51,6 +54,23 @@ internal sealed class CborWriter( encodeTagsImpl(tags) } + final override fun encodeElementTags(tags: ULongArray) { + when (rawElementTagPosition) { + RawElementTagPosition.KEY -> if (cbor.configuration.encodeKeyTags) encodeTags(tags) + RawElementTagPosition.VALUE -> if (cbor.configuration.encodeValueTags) encodeTags(tags) + } + rawElementTagPosition = RawElementTagPosition.VALUE + } + + protected fun setRawElementTagPosition(descriptor: SerialDescriptor, index: Int) { + rawElementTagPosition = + if (!descriptor.hasArrayTag() && descriptor.kind == StructureKind.MAP && index % 2 == 0) { + RawElementTagPosition.KEY + } else { + RawElementTagPosition.VALUE + } + } + protected abstract fun encodeTagsImpl(tags: ULongArray) override fun encodeByteString(byteArray: ByteArray) { @@ -181,6 +201,8 @@ internal sealed class CborWriter( } override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean { + descriptor.throwIfCborElementHasIncompatibleAnnotations(index) + setRawElementTagPosition(descriptor, index) val destination = getDestination() encodeByteArrayAsByteString = descriptor.isByteString(index) @@ -346,6 +368,8 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean { + descriptor.throwIfCborElementHasIncompatibleAnnotations(index) + setRawElementTagPosition(descriptor, index) encodeByteArrayAsByteString = descriptor.isByteString(index) //TODO check if cborelement and be done val name = descriptor.getElementName(index) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoding.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoding.kt index bf4fd9de10..82c1e3f09c 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoding.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoding.kt @@ -83,3 +83,41 @@ internal inline fun SerialDescriptor.findAnnotation(ele internal fun SerialDescriptor.getObjectTags(): ULongArray? { return annotations.filterIsInstance().firstOrNull()?.tags } + +internal fun SerialDescriptor.isCborElementDescriptor(): Boolean = + serialName.removeSuffix("?") in cborElementSerialNames + +private val cborElementSerialNames = setOf( + "kotlinx.serialization.cbor.CborElement", + "kotlinx.serialization.cbor.CborPrimitive", + "kotlinx.serialization.cbor.CborNull", + "kotlinx.serialization.cbor.CborUndefined", + "kotlinx.serialization.cbor.CborString", + "kotlinx.serialization.cbor.CborBoolean", + "kotlinx.serialization.cbor.CborByteString", + "kotlinx.serialization.cbor.CborMap", + "kotlinx.serialization.cbor.CborArray", + "kotlinx.serialization.cbor.CborDouble", + "kotlinx.serialization.cbor.CborInt", +) + +internal fun SerialDescriptor.throwIfCborElementHasIncompatibleAnnotations(index: Int) { + val elementDescriptor = getElementDescriptor(index) + if (!elementDescriptor.isCborElementDescriptor()) return + + if (getKeyTags(index) != null) { + throw SerializationException( + "KeyTags cannot be represented by a CborElement value; model the containing CborMap key directly if tagged keys are required." + ) + } + if (getValueTags(index) != null || elementDescriptor.getObjectTags() != null) { + throw SerializationException( + "CBOR tag annotations cannot be applied to CborElement properties; add tags to the CborElement instance directly." + ) + } + if (getCborLabel(index) != null) { + throw SerializationException( + "CborLabel cannot be represented by a CborElement value; model the containing CborMap key directly if numeric labels are required." + ) + } +} diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index 5af4287d28..ac3e938f80 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -425,6 +425,7 @@ class CborElementTest { @OptIn(ExperimentalStdlibApi::class) @Test fun testTagsRoundTrip() { + val cbor = Cbor { encodeValueTags = true } // Create a CborElement with tags val originalElement = CborString("Hello, tagged world!", tags = ulongArrayOf(42u)) @@ -597,138 +598,139 @@ class CborElementTest { cborInt = CborInteger(-1), tagged = 26 ), - "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472c1c3406b63626f72456c656d656e74f46f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564181aff" + "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472406b63626f72456c656d656e74f46f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564181aff" ) .let { (cbor, obj, hex) -> + val expectedDecoded = obj.copy(bStr = CborByteString(byteArrayOf())) val struct = cbor.encodeToCborElement(obj) assertEquals(hex, cbor.encodeToHexString(obj)) assertEquals(hex, cbor.encodeToHexString(struct)) assertEquals(struct, cbor.decodeFromHexString(hex)) - assertEquals(obj, cbor.decodeFromCborElement(struct)) - assertEquals(obj, cbor.decodeFromHexString(hex)) - } - - Triple( - Cbor { - encodeValueTags = true - encodeKeyTags = true - verifyKeyTags = true - verifyObjectTags = true - verifyValueTags = true - }, - MixedTag( - cborElement = CborBoolean(false), - ), - "bfd82a6b63626f72456c656d656e74d90921f4ff" - ) - .let { (cbor, obj, hex) -> - val struct = cbor.encodeToCborElement(obj) - assertEquals(hex, cbor.encodeToHexString(obj)) - assertEquals(hex, cbor.encodeToHexString(struct)) - assertEquals(struct, cbor.decodeFromHexString(hex)) - // this is ambiguous. the cborBoolean has a tag attached in the struct, coming from the valueTag (as intended), - // so now the resulting object won't have a tag but the cborElement property will have a tag attached - // hence, the following two will have: - // Expected :MixedTag(cborElement=CborPrimitive(kind=Boolean, tags=, value=false)) - // Actual :MixedTag(cborElement=CborPrimitive(kind=Boolean, tags=2337, value=false)) assertNotEquals(obj, cbor.decodeFromCborElement(struct)) assertNotEquals(obj, cbor.decodeFromHexString(hex)) + assertEquals(expectedDecoded, cbor.decodeFromCborElement(struct)) + assertEquals(expectedDecoded, cbor.decodeFromHexString(hex)) } - Triple( - Cbor { - encodeValueTags = true - encodeKeyTags = true - verifyKeyTags = true - verifyObjectTags = true - verifyValueTags = true - }, - MixedTag( - cborElement = CborBoolean(false, 90u), - ), - //valueTags first, then CborElement tags - "bfd82a6b63626f72456c656d656e74d90921d85af4ff" + + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testCborElementWithValueTagsFails() { + val cbor = Cbor { encodeValueTags = true } + val message = assertFailsWith { + cbor.encodeToByteArray(MixedValueTaggedElement.serializer(), MixedValueTaggedElement(CborBoolean(false))) + }.message + assertEquals( + "CBOR tag annotations cannot be applied to CborElement properties; add tags to the CborElement instance directly.", + message ) - .let { (cbor, obj, hex) -> - val struct = cbor.encodeToCborElement(obj) - assertEquals(hex, cbor.encodeToHexString(obj)) - assertEquals(hex, cbor.encodeToHexString(struct)) - assertEquals(struct, cbor.decodeFromHexString(hex)) - // this is ambiguous. the cborBoolean has a tag attached in the struct, coming from the valueTag (as intended), - // so now the resulting object won't have a tag but the cborElement property will have a tag attached - // hence, the following two will have: - // Expected :MixedTag(cborElement=CborPrimitive(kind=Boolean, tags=90, value=false)) - // Actual :MixedTag(cborElement=CborPrimitive(kind=Boolean, tags=2337, 90, value=false)) - //of course, the value tag verification will also fail hard - assertFailsWith( - CborDecodingException::class, - "CBOR tags [2337, 90] do not match expected tags [2337]" - ) { - assertNotEquals(obj, cbor.decodeFromCborElement(struct)) - } - assertFailsWith( - CborDecodingException::class, - "CBOR tags [2337, 90] do not match expected tags [2337]" - ) { - assertNotEquals(obj, cbor.decodeFromHexString(hex)) - } - } - Triple( - Cbor { - encodeValueTags = true - encodeKeyTags = true - verifyKeyTags = true - verifyObjectTags = true - verifyValueTags = false - }, - MixedTag( - cborElement = CborBoolean(false, 90u), - ), - //valueTags first, then CborElement tags - "bfd82a6b63626f72456c656d656e74d90921d85af4ff" + val structuredMessage = assertFailsWith { + cbor.encodeToCborElement(MixedValueTaggedElement.serializer(), MixedValueTaggedElement(CborBoolean(false))) + }.message + assertEquals( + "CBOR tag annotations cannot be applied to CborElement properties; add tags to the CborElement instance directly.", + structuredMessage ) - .let { (cbor, obj, hex) -> - val struct = cbor.encodeToCborElement(obj) - assertEquals(hex, cbor.encodeToHexString(obj)) - assertEquals(hex, cbor.encodeToHexString(struct)) - assertEquals(struct, cbor.decodeFromHexString(hex)) - // this is ambiguous. the cborBoolean has a tag attached in the struct, coming from the valueTag (as intended), - // so now the resulting object won't have a tag but the cborElement property will have a tag attached - // hence, the following two will have: - // Expected :MixedTag(cborElement=CborPrimitive(kind=Boolean, tags=90, value=false)) - // Actual :MixedTag(cborElement=CborPrimitive(kind=Boolean, tags=2337, 90, value=false)) - assertNotEquals(obj, cbor.decodeFromCborElement(struct)) - assertNotEquals(obj, cbor.decodeFromHexString(hex)) - } + } + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testCborElementWithKeyTagsFails() { + val cbor = Cbor { encodeKeyTags = true } + val message = assertFailsWith { + cbor.encodeToByteArray(MixedKeyTaggedElement.serializer(), MixedKeyTaggedElement(CborBoolean(false))) + }.message + assertEquals( + "KeyTags cannot be represented by a CborElement value; model the containing CborMap key directly if tagged keys are required.", + message + ) - Triple( - Cbor { - encodeValueTags = false - encodeKeyTags = true - verifyKeyTags = true - verifyObjectTags = true - verifyValueTags = false - }, - MixedTag( - cborElement = CborBoolean(false, 90u), - ), - "bfd82a6b63626f72456c656d656e74d85af4ff" + val structuredMessage = assertFailsWith { + cbor.encodeToCborElement(MixedKeyTaggedElement.serializer(), MixedKeyTaggedElement(CborBoolean(false))) + }.message + assertEquals( + "KeyTags cannot be represented by a CborElement value; model the containing CborMap key directly if tagged keys are required.", + structuredMessage + ) + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testConcreteCborElementWithValueTagsFails() { + val cbor = Cbor { encodeValueTags = true } + val message = assertFailsWith { + cbor.encodeToByteArray(MixedValueTaggedInteger.serializer(), MixedValueTaggedInteger(CborInteger(1))) + }.message + assertEquals( + "CBOR tag annotations cannot be applied to CborElement properties; add tags to the CborElement instance directly.", + message ) - .let { (cbor, obj, hex) -> - val struct = cbor.encodeToCborElement(obj) - assertEquals(hex, cbor.encodeToHexString(obj)) - assertEquals(hex, cbor.encodeToHexString(struct)) - assertEquals(struct, cbor.decodeFromHexString(hex)) - //no value tags means everything's fine again - assertEquals(obj, cbor.decodeFromCborElement(struct)) - assertEquals(obj, cbor.decodeFromHexString(hex)) - } + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testTaggedCborElementPropertyDecodingFails() { + val hex = cbor.encodeToHexString(CborMap(mapOf(CborString("cborElement") to CborBoolean(false)))) + val message = assertFailsWith { + cbor.decodeFromHexString(MixedValueTaggedElement.serializer(), hex) + }.message + assertEquals( + "CBOR tag annotations cannot be applied to CborElement properties; add tags to the CborElement instance directly.", + message + ) + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testKeyTaggedCborElementPropertyDecodingFails() { + val hex = cbor.encodeToHexString(CborMap(mapOf(CborString("cborElement") to CborBoolean(false)))) + val message = assertFailsWith { + cbor.decodeFromHexString(MixedKeyTaggedElement.serializer(), hex) + }.message + assertEquals( + "KeyTags cannot be represented by a CborElement value; model the containing CborMap key directly if tagged keys are required.", + message + ) + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testConcreteCborElementPropertyDecodingFails() { + val hex = cbor.encodeToHexString(CborMap(mapOf(CborString("cborElement") to CborInteger(1)))) + val message = assertFailsWith { + cbor.decodeFromHexString(MixedValueTaggedInteger.serializer(), hex) + }.message + assertEquals( + "CBOR tag annotations cannot be applied to CborElement properties; add tags to the CborElement instance directly.", + message + ) + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testCborElementTagsRemainAllowed() { + val cbor = Cbor { encodeValueTags = true } + val element = CborBoolean(false, 2337u) + val obj = MixedUntaggedElement(element) + val hex = cbor.encodeToHexString(MixedUntaggedElement.serializer(), obj) + assertEquals(obj, cbor.decodeFromHexString(MixedUntaggedElement.serializer(), hex)) + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testTaggedCborMapKeyRemainsAllowed() { + val cbor = Cbor { encodeKeyTags = true } + val element = CborMap(mapOf(CborString("key", 42u) to CborBoolean(true))) + val hex = cbor.encodeToHexString(CborElement.serializer(), element) + assertEquals(element, cbor.decodeFromHexString(CborElement.serializer(), hex)) } @OptIn(ExperimentalUnsignedTypes::class) @Test fun testCborUndefinedRoundTrip() { + val cbor = Cbor { encodeValueTags = true } val element = CborUndefined(1uL) val bytes = cbor.encodeToByteArray(element) assertEquals("c1f7", bytes.toHexString()) @@ -738,6 +740,10 @@ class CborElementTest { @OptIn(ExperimentalSerializationApi::class, ExperimentalUnsignedTypes::class) @Test fun testEncodeCborElementWritesTaggedElements() { + val cbor = Cbor { + encodeKeyTags = true + encodeValueTags = true + } val element = CborMap( mapOf( CborString("undefined") to CborUndefined(1uL), @@ -753,9 +759,418 @@ class CborElementTest { assertEquals(element, cbor.decodeFromByteArray(CborElement.serializer(), bytes)) } + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testRootCborElementTagsRespectEncodeValueTags() { + val element = CborBoolean(false, 1u) + + assertEquals("f4", cbor.encodeToHexString(CborElement.serializer(), element)) + assertEquals("c1f4", Cbor { encodeValueTags = true }.encodeToHexString(CborElement.serializer(), element)) + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testArrayCborElementTagsRespectEncodeValueTags() { + val element = CborArray(listOf(CborBoolean(false, 1u))) + + assertEquals("9ff4ff", cbor.encodeToHexString(CborElement.serializer(), element)) + assertEquals( + "9fc1f4ff", + Cbor { encodeValueTags = true }.encodeToHexString(CborElement.serializer(), element) + ) + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testMapCborElementTagsRespectKeyAndValueSwitches() { + val element = CborMap(mapOf(CborString("k", 1u) to CborBoolean(false, 2u))) + + assertEquals("bf616bf4ff", cbor.encodeToHexString(CborElement.serializer(), element)) + assertEquals( + "bfc1616bf4ff", + Cbor { encodeKeyTags = true }.encodeToHexString(CborElement.serializer(), element) + ) + assertEquals( + "bf616bc2f4ff", + Cbor { encodeValueTags = true }.encodeToHexString(CborElement.serializer(), element) + ) + assertEquals( + "bfc1616bc2f4ff", + Cbor { + encodeKeyTags = true + encodeValueTags = true + }.encodeToHexString(CborElement.serializer(), element) + ) + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testNestedCborElementTagsRespectLocalPosition() { + val element = CborMap( + mapOf( + CborString("outer") to CborMap( + mapOf(CborString("inner", 1u) to CborBoolean(false, 2u)), + 3u + ) + ) + ) + + assertEquals( + "bf656f75746572c3bf65696e6e6572c2f4ffff", + Cbor { encodeValueTags = true }.encodeToHexString(CborElement.serializer(), element) + ) + assertEquals( + "bf656f75746572bfc165696e6e6572f4ffff", + Cbor { encodeKeyTags = true }.encodeToHexString(CborElement.serializer(), element) + ) + assertEquals( + "bf656f75746572c3bfc165696e6e6572c2f4ffff", + Cbor { + encodeKeyTags = true + encodeValueTags = true + }.encodeToHexString(CborElement.serializer(), element) + ) + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testCborMapTagsRespectPositionWhenUsedAsMapKey() { + val element = CborMap(mapOf(CborMap(emptyMap(), 1u) to CborBoolean(true, 2u))) + + assertEquals("bfbffff5ff", cbor.encodeToHexString(CborElement.serializer(), element)) + assertEquals( + "bfc1bffff5ff", + Cbor { encodeKeyTags = true }.encodeToHexString(CborElement.serializer(), element) + ) + assertEquals( + "bfbfffc2f5ff", + Cbor { encodeValueTags = true }.encodeToHexString(CborElement.serializer(), element) + ) + assertEquals( + "bfc1bfffc2f5ff", + Cbor { + encodeKeyTags = true + encodeValueTags = true + }.encodeToHexString(CborElement.serializer(), element) + ) + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testEncodeToCborElementRespectsRawElementTagSwitches() { + val element = CborMap(mapOf(CborString("k", 1u) to CborBoolean(false, 2u)), 3u) + + assertEquals( + CborMap(mapOf(CborString("k") to CborBoolean(false))), + cbor.encodeToCborElement(CborElement.serializer(), element) + ) + assertEquals( + CborMap(mapOf(CborString("k", 1u) to CborBoolean(false))), + Cbor { encodeKeyTags = true }.encodeToCborElement(CborElement.serializer(), element) + ) + assertEquals( + CborMap(mapOf(CborString("k") to CborBoolean(false, 2u)), 3u), + Cbor { encodeValueTags = true }.encodeToCborElement(CborElement.serializer(), element) + ) + assertEquals( + element, + Cbor { + encodeKeyTags = true + encodeValueTags = true + }.encodeToCborElement(CborElement.serializer(), element) + ) + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testAllRootCborElementKindsRespectEncodeValueTags() { + val elements = listOf( + CborNull(1u) to CborNull(), + CborUndefined(1u) to CborUndefined(), + CborInteger(1, 1u) to CborInteger(1), + CborInteger(1uL, isPositive = false, tags = ulongArrayOf(1u)) to CborInteger(1uL, isPositive = false), + CborFloat(1.5, 1u) to CborFloat(1.5), + CborString("s", 1u) to CborString("s"), + CborBoolean(false, 1u) to CborBoolean(false), + CborByteString(byteArrayOf(1, 2), 1u) to CborByteString(byteArrayOf(1, 2)), + CborArray(listOf(CborString("v")), 1u) to CborArray(listOf(CborString("v"))), + CborMap(mapOf(CborString("k") to CborString("v")), 1u) to + CborMap(mapOf(CborString("k") to CborString("v"))), + ) + + for ((tagged, untagged) in elements) { + assertEquals( + untagged, + cbor.decodeFromByteArray(CborElement.serializer(), cbor.encodeToByteArray(CborElement.serializer(), tagged)) + ) + assertEquals( + tagged, + Cbor { encodeValueTags = true } + .decodeFromByteArray(CborElement.serializer(), Cbor { encodeValueTags = true } + .encodeToByteArray(CborElement.serializer(), tagged)) + ) + } + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testAllRawCborElementMapKeyKindsRespectEncodeKeyTags() { + val taggedKeys = listOf( + CborString("text", 1u) to CborString("text"), + CborInteger(2, 1u) to CborInteger(2), + CborByteString(byteArrayOf(3), 1u) to CborByteString(byteArrayOf(3)), + CborNull(1u) to CborNull(), + CborUndefined(1u) to CborUndefined(), + CborArray(listOf(CborInteger(4)), 1u) to CborArray(listOf(CborInteger(4))), + CborMap(emptyMap(), 1u) to CborMap(emptyMap()), + ) + val tagged = CborMap(taggedKeys.associate { (key, _) -> key to CborBoolean(true) }) + val untagged = CborMap(taggedKeys.associate { (_, key) -> key to CborBoolean(true) }) + + assertEquals( + untagged, + cbor.decodeFromByteArray(CborElement.serializer(), cbor.encodeToByteArray(CborElement.serializer(), tagged)) + ) + assertEquals( + tagged, + Cbor { encodeKeyTags = true } + .decodeFromByteArray(CborElement.serializer(), Cbor { encodeKeyTags = true } + .encodeToByteArray(CborElement.serializer(), tagged)) + ) + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testGenericSerializableRawCborElementRespectsNestedTagSwitches() { + val element = CborMap(mapOf(CborString("k", 1u) to CborBoolean(false, 2u)), 3u) + val box = GenericBox(element) + val serializer = GenericBox.serializer(CborElement.serializer()) + + assertEquals( + GenericBox(CborMap(mapOf(CborString("k") to CborBoolean(false)))), + cbor.decodeFromByteArray(serializer, cbor.encodeToByteArray(serializer, box)) + ) + assertEquals( + GenericBox(CborMap(mapOf(CborString("k", 1u) to CborBoolean(false)))), + Cbor { encodeKeyTags = true }.decodeFromByteArray( + serializer, + Cbor { encodeKeyTags = true }.encodeToByteArray(serializer, box) + ) + ) + assertEquals( + GenericBox(CborMap(mapOf(CborString("k") to CborBoolean(false, 2u)), 3u)), + Cbor { encodeValueTags = true }.decodeFromByteArray( + serializer, + Cbor { encodeValueTags = true }.encodeToByteArray(serializer, box) + ) + ) + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testGenericSerializableRawCborElementListAndMapRespectTagSwitches() { + val listBox = GenericListBox(listOf(CborString("v", 1u))) + val listSerializer = GenericListBox.serializer(CborElement.serializer()) + val mapBox = GenericMapBox(mapOf(CborInteger(1, 2u) to CborString("v", 3u))) + val mapSerializer = GenericMapBox.serializer(CborElement.serializer(), CborElement.serializer()) + + assertEquals( + GenericListBox(listOf(CborString("v"))), + cbor.decodeFromByteArray(listSerializer, cbor.encodeToByteArray(listSerializer, listBox)) + ) + assertEquals( + GenericListBox(listOf(CborString("v", 1u))), + Cbor { encodeValueTags = true }.decodeFromByteArray( + listSerializer, + Cbor { encodeValueTags = true }.encodeToByteArray(listSerializer, listBox) + ) + ) + assertEquals( + GenericMapBox(mapOf(CborInteger(1, 2u) to CborString("v", 3u))), + Cbor { + encodeKeyTags = true + encodeValueTags = true + }.decodeFromByteArray( + mapSerializer, + Cbor { + encodeKeyTags = true + encodeValueTags = true + }.encodeToByteArray(mapSerializer, mapBox) + ) + ) + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testGenericSerializableConcreteCborElementSubtypesRespectTagSwitches() { + val mapBox = GenericBox(CborMap(mapOf(CborString("k", 1u) to CborString("v", 2u)), 3u)) + val mapSerializer = GenericBox.serializer(CborMap.serializer()) + val arrayBox = GenericBox(CborArray(listOf(CborString("v", 1u)), 2u)) + val arraySerializer = GenericBox.serializer(CborArray.serializer()) + val stringBox = GenericBox(CborString("v", 1u)) + val stringSerializer = GenericBox.serializer(CborString.serializer()) + + assertEquals( + GenericBox(CborMap(mapOf(CborString("k") to CborString("v")))), + cbor.decodeFromByteArray(mapSerializer, cbor.encodeToByteArray(mapSerializer, mapBox)) + ) + assertEquals( + mapBox, + Cbor { + encodeKeyTags = true + encodeValueTags = true + }.decodeFromByteArray( + mapSerializer, + Cbor { + encodeKeyTags = true + encodeValueTags = true + }.encodeToByteArray(mapSerializer, mapBox) + ) + ) + assertEquals( + GenericBox(CborArray(listOf(CborString("v")))), + cbor.decodeFromByteArray(arraySerializer, cbor.encodeToByteArray(arraySerializer, arrayBox)) + ) + assertEquals( + arrayBox, + Cbor { encodeValueTags = true }.decodeFromByteArray( + arraySerializer, + Cbor { encodeValueTags = true }.encodeToByteArray(arraySerializer, arrayBox) + ) + ) + assertEquals( + GenericBox(CborString("v")), + cbor.decodeFromByteArray(stringSerializer, cbor.encodeToByteArray(stringSerializer, stringBox)) + ) + assertEquals( + stringBox, + Cbor { encodeValueTags = true }.decodeFromByteArray( + stringSerializer, + Cbor { encodeValueTags = true }.encodeToByteArray(stringSerializer, stringBox) + ) + ) + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testCborLabelOnCborElementPropertyFails() { + val box = LabelledRawElementBox(CborString("x")) + val cbor = Cbor { + preferCborLabelsOverNames = true + encodeKeyTags = true + encodeValueTags = true + } + + val encodeMessage = assertFailsWith { + cbor.encodeToByteArray(LabelledRawElementBox.serializer(), box) + }.message + assertEquals( + "CborLabel cannot be represented by a CborElement value; model the containing CborMap key directly if numeric labels are required.", + encodeMessage + ) + + val structuredMessage = assertFailsWith { + cbor.encodeToCborElement(LabelledRawElementBox.serializer(), box) + }.message + assertEquals( + "CborLabel cannot be represented by a CborElement value; model the containing CborMap key directly if numeric labels are required.", + structuredMessage + ) + + val decodeMessage = assertFailsWith { + cbor.decodeFromCborElement(LabelledRawElementBox.serializer(), CborMap(mapOf(CborInteger(1) to CborString("x")))) + }.message + assertEquals( + "CborLabel cannot be represented by a CborElement value; model the containing CborMap key directly if numeric labels are required.", + decodeMessage + ) + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testCborLabelOnConcreteCborElementPropertyFails() { + val message = assertFailsWith { + cbor.encodeToByteArray(LabelledRawIntegerBox.serializer(), LabelledRawIntegerBox(CborInteger(1))) + }.message + assertEquals( + "CborLabel cannot be represented by a CborElement value; model the containing CborMap key directly if numeric labels are required.", + message + ) + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testRawCborMapNumericKeyRemainsAllowed() { + val element = CborMap(mapOf(CborInteger(1) to CborString("x"))) + val encoded = cbor.encodeToByteArray(CborElement.serializer(), element) + assertEquals(element, cbor.decodeFromByteArray(CborElement.serializer(), encoded)) + assertEquals( + element, + cbor.decodeFromCborElement(CborElement.serializer(), CborMap(mapOf(CborInteger(1) to CborString("x")))) + ) + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testCborLabelDoesNotMakeTaggedCborElementPropertyAllowed() { + val valueMessage = assertFailsWith { + cbor.encodeToByteArray(LabelledValueTaggedElement.serializer(), LabelledValueTaggedElement(CborBoolean(false))) + }.message + assertEquals( + "CBOR tag annotations cannot be applied to CborElement properties; add tags to the CborElement instance directly.", + valueMessage + ) + + val keyMessage = assertFailsWith { + cbor.encodeToByteArray(LabelledKeyTaggedElement.serializer(), LabelledKeyTaggedElement(CborBoolean(false))) + }.message + assertEquals( + "KeyTags cannot be represented by a CborElement value; model the containing CborMap key directly if tagged keys are required.", + keyMessage + ) + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testDecodingRawCborElementPreservesTagsIndependentOfEncodeSwitches() { + assertEquals(CborBoolean(false, 1u), cbor.decodeFromHexString(CborElement.serializer(), "c1f4")) + assertEquals( + CborMap(mapOf(CborString("k", 1u) to CborBoolean(false, 2u))), + cbor.decodeFromHexString(CborElement.serializer(), "bfc1616bc2f4ff") + ) + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testConcreteCborElementSerializersRespectRawTagSwitches() { + val string = CborString("v", 1u) + assertEquals(CborString("v"), cbor.decodeFromByteArray(CborString.serializer(), cbor.encodeToByteArray(string))) + assertEquals( + string, + Cbor { encodeValueTags = true } + .decodeFromByteArray(CborString.serializer(), Cbor { encodeValueTags = true }.encodeToByteArray(string)) + ) + + val map = CborMap(mapOf(CborString("k", 1u) to CborString("v", 2u)), 3u) + assertEquals( + CborMap(mapOf(CborString("k", 1u) to CborString("v", 2u)), 3u), + Cbor { + encodeKeyTags = true + encodeValueTags = true + }.decodeFromByteArray( + CborMap.serializer(), + Cbor { + encodeKeyTags = true + encodeValueTags = true + }.encodeToByteArray(CborMap.serializer(), map) + ) + ) + } + @OptIn(ExperimentalUnsignedTypes::class) @Test fun testTagsPreservedWhenDecodingTypedElements() { + val cbor = Cbor { encodeValueTags = true } val taggedMap = CborMap(mapOf(CborString("a") to CborInteger(1)), 1uL) assertEquals(taggedMap, cbor.decodeFromByteArray(cbor.encodeToByteArray(taggedMap))) @@ -785,8 +1200,65 @@ data class MixedBag( @Serializable -data class MixedTag( +data class MixedValueTaggedElement( + @ValueTags(2337u) + val cborElement: CborElement?, +) + +@Serializable +data class MixedKeyTaggedElement( @KeyTags(42u) + val cborElement: CborElement?, +) + +@Serializable +data class MixedValueTaggedInteger( @ValueTags(2337u) + val cborElement: CborInteger, +) + +@Serializable +data class MixedUntaggedElement( val cborElement: CborElement?, ) + +@Serializable +data class GenericBox( + val value: T, +) + +@Serializable +data class GenericListBox( + val values: List, +) + +@Serializable +data class GenericMapBox( + val values: Map, +) + +@Serializable +data class LabelledRawElementBox( + @CborLabel(1) + val value: CborElement, +) + +@Serializable +data class LabelledRawIntegerBox( + @CborLabel(1) + val value: CborInteger, +) + +@Serializable +data class LabelledValueTaggedElement( + @CborLabel(1) + @ValueTags(2337u) + val value: CborElement, +) + +@Serializable +data class LabelledKeyTaggedElement( + @CborLabel(1) + @KeyTags(42u) + val value: CborElement, +) From 61cbae1cc34b63bfdb3beabec9ca5a132895c12f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Wed, 20 May 2026 07:27:56 +0200 Subject: [PATCH 66/68] CBOR: update docs --- docs/formats.md | 55 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/docs/formats.md b/docs/formats.md index 7b489f43dc..9472ee4902 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -315,9 +315,12 @@ This may be used to encode COSE structures, see [RFC 9052 2. Basic COSE Structur ### Custom CBOR-specific Serializers Cbor encoders and decoders implement the interfaces [CborEncoder](CborEncoder.kt) and [CborDecoder](CborDecoder.kt), respectively. -These interfaces contain a single property, `cbor`, exposing the current CBOR serialization configuration. -This enables custom cbor-specific serializers to reuse the current `Cbor` instance to produce embedded byte arrays or -react to configuration settings such as `preferCborLabelsOverNames` or `useDefiniteLengthEncoding`, for example. +These interfaces expose the current CBOR serialization configuration through the `cbor` property. +In addition, [CborEncoder](CborEncoder.kt) can encode a complete [CborElement] with `encodeCborElement`, and +[CborDecoder](CborDecoder.kt) can decode the next data item as a [CborElement] with `decodeCborElement`. +This enables custom cbor-specific serializers to reuse the current `Cbor` instance to produce embedded byte arrays, +encode or decode a whole CBOR subtree, or react to configuration settings such as `preferCborLabelsOverNames` or +`useDefiniteLengthEncoding`, for example. ### CBOR Elements @@ -354,9 +357,18 @@ CborMap(tags=[], content={CborString(tags=[], value=bytes)=CborByteString(tags=[ #### Tagging `CborElement`s Every CborElement—whether it is used as a property, a value inside a collection, or even a complex key inside a map -(which is perfectly legal in CBOR)—supports tags. Tags can be specified by passing them s varargs parameters upon -CborElement creation. -For example, take following structure (represented in diagnostic notation): +(which is perfectly legal in CBOR)—supports tags. Tags can be specified by passing them as vararg parameters upon +CborElement creation. + +When encoding raw [CborElement] instances, the usual tag configuration switches still apply. Tags on a root element, +on an array item, or on a map value are encoded only when `encodeValueTags` is enabled. Tags on a map key are encoded +only when `encodeKeyTags` is enabled. Decoding a [CborElement] always preserves tags present in the input. + +Tag and label annotations describe properties of serializable classes, while [CborElement] already models CBOR data +directly. For that reason, `@KeyTags`, `@ValueTags`, and `@CborLabel` cannot be applied to [CborElement]-typed +properties. Put value tags on the `CborElement.tags` array directly, and model tagged or numeric keys as [CborMap] +keys such as `CborString("key", 42u)` or `CborInteger(1)`. +For example, take the following structure (represented in diagnostic notation): @@ -390,20 +402,20 @@ bf # map(*) Decoding it results in the following CborElement (shown in manually formatted diagnostic notation): ``` -CborMap(tags=[], content={ - CborString(tags=[], value=a) = CborPositiveInt( tags=[12], value=268435455), - CborString(tags=[34], value=b) = CborNegativeInt( tags=[], value=-1), - CborString(tags=[56], value=c) = CborByteString( tags=[78], bytes=h'cafe), - CborString(tags=[], value=d) = CborString( tags=[90, 12], value=Hello World) +CborMap(tags=[], content={ + CborString(tags=[], value=a) = CborInt(tags=[12], absoluteValue = 268435455 ), + CborString(tags=[34], value=b) = CborInt(tags=[], absoluteValue = -1 ), + CborString(tags=[56], value=c) = CborByteString(tags=[78], bytes = h'cafe ), + CborString(tags=[], value=d) = CborString(tags=[90, 12], value = Hello World ) }) ``` ##### Caution -Tags are properties of `CborElements`, and it is possible to mixing arbitrary serializable values with `CborElement`s that -contain tags inside a serializable structure. It is also possible to annotate any [CborElement] property -of a generic serializable class with `@ValueTags`. -**This can lead to asymmetric behavior when serializing and deserializing such structures!** +Tags are properties of `CborElement`s, and it is possible to mix arbitrary serializable values with `CborElement`s that +contain tags inside a serializable structure. Be aware that raw `CborElement.tags` are controlled by `encodeKeyTags` or +`encodeValueTags` depending on the element position. If the matching switch is disabled, encoded output omits those tags, +and decoding that output will produce an otherwise equal [CborElement] without the omitted tags. #### Types of CBOR Elements @@ -411,13 +423,18 @@ A [CborElement] class has three direct subtypes, closely following CBOR grammar: * [CborPrimitive] represents primitive CBOR elements, such as string, integer, float, boolean, and null. CBOR byte strings are also treated as primitives. - Concrete primitive types expose dedicated accessors where their CBOR content maps cleanly to Kotlin values. + [CborElement] and [CborPrimitive] do not expose a generic `value` property. Concrete primitive types expose dedicated + accessors where their CBOR content maps cleanly to Kotlin values. Note that Cbor discriminates between positive ("unsigned") and negative ("signed") integers! `CborPrimitive` is itself an umbrella type (a sealed class) for the following concrete primitives: * [CborNull] mapping to a Kotlin `null` + * [CborUndefined] mapping to CBOR `undefined` * [CborBoolean] mapping to a Kotlin `Boolean` - * [CborInt] represents signed CBOR integer (major type 1 encompassing `-2^64..-1`) and unsigned CBOR integer (major type 0 encompassing `0..2^64-1`). - Since this exceeds the range of Kotlin's built-in `Long` type, CborInt consists of `isPositive` and `absoluteValue` representing the absolute value as an `ULong`. It also features `long`, `int`, `short`, and `byte` conversion properties that throw when the value cannot be represented in the requested Kotlin type. + * [CborInteger] represents signed CBOR integer (major type 1 encompassing `-2^64..-1`) and unsigned CBOR integer (major + type 0 encompassing `0..2^64-1`). Since this exceeds the range of Kotlin's built-in `Long` type, CborInteger features + `isPositive` and `absoluteValue` representing the absolute value as an `ULong`. + It also features `long`, `int`, `short`, and `byte` conversion properties that throw when the value cannot be + represented in the requested Kotlin type. * [CborString] maps to a Kotlin `String` * [CborFloat] maps to Kotlin `Double` * [CborByteString] maps to a Kotlin `ByteArray` and is used to encode them as CBOR byte string (in contrast to a list @@ -1791,7 +1808,9 @@ This chapter concludes [Kotlin Serialization Guide](serialization-guide.md). [Cbor.encodeToCborElement]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/encode-to-cbor-element.html [CborPrimitive]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-primitive/index.html [CborNull]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-null/index.html +[CborUndefined]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-undefined/index.html [CborBoolean]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-boolean/index.html +[CborInteger]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-integer/index.html [CborString]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-string/index.html [CborFloat]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-float/index.html [CborByteString]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-cbor/kotlinx.serialization.cbor/-cbor-byte-string/index.html From 01bba13aacd2db73d9c8851e5536216bdd5867d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Tue, 26 May 2026 09:53:02 +0200 Subject: [PATCH 67/68] CBOR: safer wrapping of mutable tags and bytestring + convenience copy methods+ctors --- .../kotlinx/serialization/cbor/CborElement.kt | 270 +++++++++++++++--- .../serialization/cbor/DelicateCborApi.kt | 10 + .../cbor/internal/CborElementSerializers.kt | 20 +- .../serialization/cbor/internal/Decoder.kt | 9 +- .../serialization/cbor/internal/Encoder.kt | 5 +- .../cbor/CborDelegatedPropertiesTest.kt | 9 +- .../serialization/cbor/CborElementTest.kt | 89 ++---- 7 files changed, 291 insertions(+), 121 deletions(-) create mode 100644 formats/cbor/commonMain/src/kotlinx/serialization/cbor/DelicateCborApi.kt diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt index 974ced5516..13960ac6d5 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -1,10 +1,11 @@ @file:Suppress("unused") -@file:OptIn(ExperimentalUnsignedTypes::class) +@file:OptIn(ExperimentalUnsignedTypes::class, DelicateCborApi::class, ExperimentalSerializationApi::class) package kotlinx.serialization.cbor import kotlinx.serialization.* import kotlinx.serialization.cbor.internal.* +import kotlin.jvm.* @OptIn(ExperimentalUnsignedTypes::class) internal val EMPTY_TAGS: ULongArray = ULongArray(0) @@ -20,7 +21,6 @@ internal val EMPTY_TAGS: ULongArray = ULongArray(0) * The whole hierarchy is [serializable][Serializable] only by [Cbor] format. */ @Serializable(with = CborElementSerializer::class) -@ExperimentalSerializationApi public sealed class CborElement( /** * CBOR tags associated with this element. @@ -31,26 +31,30 @@ public sealed class CborElement( tags: ULongArray = EMPTY_TAGS ) { + + @OptIn(ExperimentalUnsignedTypes::class) + @DelicateCborApi + internal var rawTags: ULongArray = tags + /** * CBOR tags associated with this element. * Tags are optional semantic tagging of other major types (major type 6). * See [RFC 8949 3.4. Tagging of Items](https://datatracker.ietf.org/doc/html/rfc8949#name-tagging-of-items). */ - @OptIn(ExperimentalUnsignedTypes::class) - public var tags: ULongArray = tags - internal set //need this to collect + public val tags: List by lazy { rawTags.toList() } + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is CborElement) return false - if (!tags.contentEquals(other.tags)) return false + if (!rawTags.contentEquals(other.rawTags)) return false return true } override fun hashCode(): Int { - return tags.contentHashCode() + return rawTags.contentHashCode() } } @@ -60,7 +64,6 @@ public sealed class CborElement( * CBOR primitives include numbers, strings, booleans, byte arrays and special null value [CborNull]. */ @Serializable(with = CborPrimitiveSerializer::class) -@ExperimentalSerializationApi public sealed class CborPrimitive( tags: ULongArray = EMPTY_TAGS ) : CborElement(tags) @@ -73,7 +76,6 @@ public sealed class CborPrimitive( * depending on the value of [isPositive]. Note that [absoluteValue] **must not be** `0` when [isPositive] is set to `false`. */ @Serializable(with = CborIntSerializer::class) -@ExperimentalSerializationApi public class CborInteger( public val absoluteValue: ULong, public val isPositive: Boolean, @@ -102,10 +104,10 @@ public class CborInteger( } override fun toString(): String { - return "CborInt(tags=${tags.joinToString(prefix = "[", postfix = "]")}, " + + return "CborInt(tags=${rawTags.joinToString(prefix = "[", postfix = "]")}, " + "absoluteValue=" + when (isPositive) { - true -> "" - false -> "-" + true -> "" + false -> "-" } + absoluteValue + ")" @@ -120,7 +122,6 @@ public class CborInteger( * depending on whether a positive or a negative number was passed. * If you want to create a negative number exceeding [Long.MIN_VALUE], manually specify sign: `CborInt(ULong.MAX_VALUE, isPositive = false)`. */ -@ExperimentalSerializationApi @Suppress("FunctionName") public fun CborInteger(value: Long, vararg tags: ULong): CborInteger = if (value >= 0L) CborInteger(value.toULong(), isPositive = true, tags = tags) @@ -129,7 +130,6 @@ public fun CborInteger(value: Long, vararg tags: ULong): CborInteger = /** * Creates an unsigned CBOR integer (major type 0). */ -@ExperimentalSerializationApi @Suppress("FunctionName") public fun CborInteger(value: ULong, vararg tags: ULong): CborInteger = CborInteger(value, isPositive = true, tags = tags) @@ -137,14 +137,12 @@ public fun CborInteger(value: ULong, vararg tags: ULong): CborInteger = /** * Converts this integer to [Long], throwing if it cannot be represented as [Long]. */ -@ExperimentalSerializationApi public val CborInteger.long: Long get() = longOrNull ?: throw ArithmeticException("$this cannot be represented as Long") /** * Converts this integer to [Long], or returns `null` if it cannot be represented as [Long]. */ -@ExperimentalSerializationApi public val CborInteger.longOrNull: Long? get() { val max = Long.MAX_VALUE.toULong() @@ -162,14 +160,12 @@ public val CborInteger.longOrNull: Long? /** * Converts this integer to [Int], throwing if it cannot be represented as [Int]. */ -@ExperimentalSerializationApi public val CborInteger.int: Int get() = intOrNull ?: throw ArithmeticException("$this cannot be represented as Int") /** * Converts this integer to [Int], or returns `null` if it cannot be represented as [Int]. */ -@ExperimentalSerializationApi public val CborInteger.intOrNull: Int? get() { val longValue = longOrNull ?: return null @@ -180,14 +176,12 @@ public val CborInteger.intOrNull: Int? /** * Converts this integer to [Short], throwing if it cannot be represented as [Short]. */ -@ExperimentalSerializationApi public val CborInteger.short: Short get() = shortOrNull ?: throw ArithmeticException("$this cannot be represented as Short") /** * Converts this integer to [Short], or returns `null` if it cannot be represented as [Short]. */ -@ExperimentalSerializationApi public val CborInteger.shortOrNull: Short? get() { val longValue = longOrNull ?: return null @@ -198,14 +192,12 @@ public val CborInteger.shortOrNull: Short? /** * Converts this integer to [Byte], throwing if it cannot be represented as [Byte]. */ -@ExperimentalSerializationApi public val CborInteger.byte: Byte get() = byteOrNull ?: throw ArithmeticException("$this cannot be represented as Byte") /** * Converts this integer to [Byte], or returns `null` if it cannot be represented as [Byte]. */ -@ExperimentalSerializationApi public val CborInteger.byteOrNull: Byte? get() { val longValue = longOrNull ?: return null @@ -217,7 +209,6 @@ public val CborInteger.byteOrNull: Byte? * Class representing CBOR floating point value (major type 7). */ @Serializable(with = CborFloatSerializer::class) -@ExperimentalSerializationApi public class CborFloat( public val value: Double, vararg tags: ULong @@ -233,7 +224,7 @@ public class CborFloat( override fun toString(): String { return "CborFloat(" + - "tags=${tags.joinToString(prefix = "[", postfix = "]")}, " + + "tags=${rawTags.joinToString(prefix = "[", postfix = "]")}, " + "value=$value" + ")" } @@ -243,7 +234,6 @@ public class CborFloat( * Class representing CBOR string value. */ @Serializable(with = CborStringSerializer::class) -@ExperimentalSerializationApi public class CborString( public val value: String, vararg tags: ULong @@ -259,7 +249,7 @@ public class CborString( override fun toString(): String { return "CborString(" + - "tags=${tags.joinToString(prefix = "[", postfix = "]")}, " + + "tags=${rawTags.joinToString(prefix = "[", postfix = "]")}, " + "value=$value" + ")" } @@ -269,11 +259,22 @@ public class CborString( * Class representing CBOR boolean value. */ @Serializable(with = CborBooleanSerializer::class) -@ExperimentalSerializationApi public class CborBoolean( public val value: Boolean, vararg tags: ULong ) : CborPrimitive(tags) { + + public constructor(value: Boolean, tags: List) : this(value, *(tags.toULongArray())) + + /** + * Creates a [CborBoolean] from the provided [CborBoolean]'s [value], and the specified [tags]. + * + * @param value The [CborBoolean] instance whose boolean value is used to initialize this object. + * @param tags A list of tags to be set with the new instance. + */ + public constructor(value: CborBoolean, tags: List) : this(value.value, *(tags.toULongArray())) + public constructor(value: CborBoolean, vararg tags: ULong) : this(value.value, *tags) + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is CborBoolean) return false @@ -285,7 +286,7 @@ public class CborBoolean( override fun toString(): String { return "CborBoolean(" + - "tags=${tags.joinToString(prefix = "[", postfix = "]")}, " + + "tags=${rawTags.joinToString(prefix = "[", postfix = "]")}, " + "value=$value" + ")" } @@ -295,32 +296,37 @@ public class CborBoolean( * Class representing CBOR byte string value. */ @Serializable(with = CborByteStringSerializer::class) -@ExperimentalSerializationApi public class CborByteString( - private val bytes: ByteArray, + bytes: ByteArray, vararg tags: ULong ) : CborPrimitive(tags) { + + public constructor(bytes: ByteArray, tags: List) : this(bytes, *(tags.toULongArray())) + + @DelicateCborApi + public val bytes: ByteArray = bytes + /** - * Returns a copy of this CBOR byte string contents. + * Returns a deep copy of this CBOR byte string contents. */ public fun toByteArray(): ByteArray = bytes.copyOf() override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is CborByteString) return false - if (!tags.contentEquals(other.tags)) return false + if (!rawTags.contentEquals(other.rawTags)) return false return bytes.contentEquals(other.bytes) } override fun hashCode(): Int { - var result = tags.contentHashCode() + var result = rawTags.contentHashCode() result = 31 * result + (bytes.contentHashCode()) return result } override fun toString(): String { return "CborByteString(" + - "tags=${tags.joinToString(prefix = "[", postfix = "]")}, " + + "tags=${rawTags.joinToString(prefix = "[", postfix = "]")}, " + "bytes=h'${bytes.toHexString()}" + ")" } @@ -332,8 +338,10 @@ public class CborByteString( * Class representing CBOR `null` value */ @Serializable(with = CborNullSerializer::class) -@ExperimentalSerializationApi public class CborNull(vararg tags: ULong) : CborPrimitive(tags) { + + public constructor(tags: List) : this(*(tags.toULongArray())) + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is CborNull) return false @@ -343,7 +351,7 @@ public class CborNull(vararg tags: ULong) : CborPrimitive(tags) { override fun hashCode(): Int = CborNull::class.hashCode() * 31 + super.hashCode() override fun toString(): String { - return "CborNull(tags=${tags.joinToString(prefix = "[", postfix = "]")})" + return "CborNull(tags=${rawTags.joinToString(prefix = "[", postfix = "]")})" } } @@ -351,8 +359,10 @@ public class CborNull(vararg tags: ULong) : CborPrimitive(tags) { * Class representing CBOR `undefined` value */ @Serializable(with = CborUndefinedSerializer::class) -@ExperimentalSerializationApi public class CborUndefined(vararg tags: ULong) : CborPrimitive(tags) { + + public constructor(tags: List) : this(*(tags.toULongArray())) + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is CborUndefined) return false @@ -362,7 +372,7 @@ public class CborUndefined(vararg tags: ULong) : CborPrimitive(tags) { override fun hashCode(): Int = CborUndefined::class.hashCode() * 31 + super.hashCode() override fun toString(): String { - return "CborUndefined(tags=${tags.joinToString(prefix = "[", postfix = "]")})" + return "CborUndefined(tags=${rawTags.joinToString(prefix = "[", postfix = "]")})" } } @@ -373,20 +383,24 @@ public class CborUndefined(vararg tags: ULong) : CborPrimitive(tags) { * traditional methods like [Map.get] or [Map.getValue] to obtain CBOR elements. */ @Serializable(with = CborMapSerializer::class) -@ExperimentalSerializationApi public class CborMap( private val content: Map, vararg tags: ULong ) : CborElement(tags), Map by content { + public constructor(content: Map, tags: List) : this( + content, + *(tags.toULongArray()) + ) + public override fun equals(other: Any?): Boolean = - other is CborMap && other.content == content && other.tags.contentEquals(tags) + other is CborMap && other.content == content && other.rawTags.contentEquals(rawTags) - public override fun hashCode(): Int = content.hashCode() * 31 + tags.contentHashCode() + public override fun hashCode(): Int = content.hashCode() * 31 + rawTags.contentHashCode() override fun toString(): String { return "CborMap(" + - "tags=${tags.joinToString(prefix = "[", postfix = "]")}, " + + "tags=${rawTags.joinToString(prefix = "[", postfix = "]")}, " + "content=$content" + ")" } @@ -400,6 +414,26 @@ public class CborMap( public operator fun get(key: Int): CborElement? = content[CborInteger(key.toLong())] public fun getValue(key: Int): CborElement = content.getValue(CborInteger(key.toLong())) + public companion object { + //these are inside the companion to avoid name clashes on the JVM + + @JvmName("invokeString") + public operator fun invoke(content: Map, tags: List): CborMap = + CborMap(content.mapKeys { (k, _) -> CborString(k) }, *(tags.toULongArray())) + + @JvmName("invokeLong") + public operator fun invoke(content: Map, tags: List): CborMap = + CborMap(content.mapKeys { (k, _) -> CborInteger(k) }, *(tags.toULongArray())) + + @JvmName("invokeStringVarargs") + public operator fun invoke(content: Map, vararg tags: ULong): CborMap = + CborMap(content.mapKeys { (k, _) -> CborString(k) }, *tags) + + @JvmName("invokeLongVarargs") + public operator fun invoke(content: Map, vararg tags: ULong): CborMap = + CborMap(content.mapKeys { (k, _) -> CborInteger(k) }, *tags) + } + } /** @@ -409,22 +443,166 @@ public class CborMap( * traditional methods like [List.get] or [List.size] to obtain CBOR elements. */ @Serializable(with = CborArraySerializer::class) -@ExperimentalSerializationApi public class CborArray( private val content: List, vararg tags: ULong ) : CborElement(tags), List by content { + public constructor(content: List, tags: List) : this(content, *(tags.toULongArray())) + public constructor(content: Array, vararg tags: ULong) : this(content.toList(), *tags) + public constructor(content: Array, tags: List) : this(content, *(tags.toULongArray())) + public override fun equals(other: Any?): Boolean = - other is CborArray && other.content == content && other.tags.contentEquals(tags) + other is CborArray && other.content == content && other.rawTags.contentEquals(rawTags) - public override fun hashCode(): Int = content.hashCode() * 31 + tags.contentHashCode() + public override fun hashCode(): Int = content.hashCode() * 31 + rawTags.contentHashCode() override fun toString(): String { return "CborArray(" + - "tags=${tags.joinToString(prefix = "[", postfix = "]")}, " + + "tags=${rawTags.joinToString(prefix = "[", postfix = "]")}, " + "content=$content" + ")" } } + +/** + * Creates a copy of this [CborPrimitive] with the specified [tags], discarding any existing tags. + */ +@OptIn(ExperimentalSerializationApi::class) +@Suppress("UNCHECKED_CAST") +public fun T.copy(vararg tags: ULong): T = + when (this) { + is CborBoolean -> CborBoolean(value, *tags) + is CborByteString -> CborByteString(toByteArray(), *tags) + is CborFloat -> CborFloat(value, *tags) + is CborInteger -> CborInteger(absoluteValue, isPositive, *tags) + is CborNull -> CborNull(*tags) + is CborString -> CborString(value, *tags) + is CborUndefined -> CborUndefined(*tags) + is CborArray -> CborArray(toList(), *tags) + is CborMap -> CborMap(toMap(), *tags) + } as T + +/** + * Creates a copy of this [CborPrimitive] with the specified [tags], discarding any existing tags. + */ +@OptIn(ExperimentalSerializationApi::class) +public fun T.copy(tags: List): T = copy(*tags.toULongArray()) + + +/*START BOOLEAN*/ +/** Creates copy of this [CborBoolean] with the specified [value], copying all tags.*/ +public fun CborBoolean.copy(value: Boolean): CborBoolean = CborBoolean(value, *(rawTags.copyOf())) +/** Creates copy of this [CborBoolean] with the specified [value] and [tags].*/ +public fun CborBoolean.copy(value: Boolean, vararg tags: ULong): CborBoolean = CborBoolean(value, *tags) +/** Creates copy of this [CborBoolean] with the specified [value] and [tags].*/ +public fun CborBoolean.copy(value: Boolean, tags: List): CborBoolean = copy(value, *tags.toULongArray()) +/*END BOOLEAN*/ + +/*START INTEGER*/ +/** Creates copy of this [CborInteger] with the specified [value], copying all tags.*/ +public fun CborInteger.copy(value: Long): CborInteger = CborInteger(value, *(rawTags.copyOf())) +/** Creates copy of this [CborInteger] with the specified [value] and [tags].*/ +public fun CborInteger.copy(value: Long, vararg tags: ULong): CborInteger = CborInteger(value, *tags) +/** Creates copy of this [CborInteger] with the specified [value] and [tags].*/ +public fun CborInteger.copy(value: Long, tags: List): CborInteger = copy(value, *tags.toULongArray()) + +/** Creates copy of this [CborInteger] with the specified [absoluteValue] and [isPositive], copying all tags.*/ +public fun CborInteger.copy(absoluteValue: ULong, isPositive: Boolean): CborInteger = + CborInteger(absoluteValue, isPositive, *(rawTags.copyOf())) +/** Creates copy of this [CborInteger] with the specified [absoluteValue], [isPositive] and [tags].*/ +public fun CborInteger.copy(absoluteValue: ULong, isPositive: Boolean, vararg tags: ULong): CborInteger = + CborInteger(absoluteValue, isPositive, *tags) +/** Creates copy of this [CborInteger] with the specified [absoluteValue], [isPositive] and [tags].*/ +public fun CborInteger.copy(absoluteValue: ULong, isPositive: Boolean, tags: List): CborInteger = + copy(absoluteValue, isPositive, *tags.toULongArray()) +/*END INTEGER*/ + +/*START FLOAT*/ +/** Creates copy of this [CborFloat] with the specified [value], copying all tags.*/ +public fun CborFloat.copy(value: Double): CborFloat = CborFloat(value, *(rawTags.copyOf())) +/** Creates copy of this [CborFloat] with the specified [value] and [tags].*/ +public fun CborFloat.copy(value: Double, vararg tags: ULong): CborFloat = CborFloat(value, *tags) +/** Creates copy of this [CborFloat] with the specified [value] and [tags].*/ +public fun CborFloat.copy(value: Double, tags: List): CborFloat = copy(value, *tags.toULongArray()) +/*END FLOAT*/ + +/*START STRING*/ +/** Creates copy of this [CborString] with the specified [value], copying all tags.*/ +public fun CborString.copy(value: String): CborString = CborString(value, *(rawTags.copyOf())) +/** Creates copy of this [CborString] with the specified [value] and [tags].*/ +public fun CborString.copy(value: String, vararg tags: ULong): CborString = CborString(value, *tags) +/** Creates copy of this [CborString] with the specified [value] and [tags].*/ +public fun CborString.copy(value: String, tags: List): CborString = copy(value, *tags.toULongArray()) +/*END STRING*/ + +/*START BYTE STRING*/ +/** Creates copy of this [CborByteString] with the specified [bytes], copying all tags.*/ +public fun CborByteString.copy(bytes: ByteArray): CborByteString = CborByteString(bytes, *(rawTags.copyOf())) +/** Creates copy of this [CborByteString] with the specified [bytes] and [tags].*/ +public fun CborByteString.copy(bytes: ByteArray, vararg tags: ULong): CborByteString = CborByteString(bytes, *tags) +/** Creates copy of this [CborByteString] with the specified [bytes] and [tags].*/ +public fun CborByteString.copy(bytes: ByteArray, tags: List): CborByteString = + copy(bytes, *tags.toULongArray()) +/*END BYTE STRING*/ + + +/*START MAP*/ +/** Creates copy of this [CborMap] with the specified [content], copying all tags.*/ +public fun CborMap.copy(content: Map): CborMap = CborMap(content, *(rawTags.copyOf())) +/** Creates copy of this [CborMap] with the specified [content] and [tags].*/ +public fun CborMap.copy(content: Map, vararg tags: ULong): CborMap = CborMap(content, *tags) +/** Creates copy of this [CborMap] with the specified [content] and [tags].*/ +public fun CborMap.copy(content: Map, tags: List): CborMap = + copy(content, *tags.toULongArray()) + +/** Creates copy of this [CborMap] with the specified [content], copying all tags.*/ +@JvmName("copyStringMap") +public fun CborMap.copy(content: Map): CborMap = + CborMap(content.mapKeys { (k, _) -> CborString(k) }, *(rawTags.copyOf())) + +/** Creates copy of this [CborMap] with the specified [content] and [tags].*/ +@JvmName("copyStringMapVarargs") +public fun CborMap.copy(content: Map, vararg tags: ULong): CborMap = + CborMap(content.mapKeys { (k, _) -> CborString(k) }, *tags) + +/** Creates copy of this [CborMap] with the specified [content] and [tags].*/ +@JvmName("copyStringMapList") +public fun CborMap.copy(content: Map, tags: List): CborMap = + copy(content, *tags.toULongArray()) + +/** Creates copy of this [CborMap] with the specified [content], copying all tags.*/ +@JvmName("copyLongMap") +public fun CborMap.copy(content: Map): CborMap = + CborMap(content.mapKeys { (k, _) -> CborInteger(k) }, *(rawTags.copyOf())) + +/** Creates copy of this [CborMap] with the specified [content] and [tags].*/ +@JvmName("copyLongMapVarargs") +public fun CborMap.copy(content: Map, vararg tags: ULong): CborMap = + CborMap(content.mapKeys { (k, _) -> CborInteger(k) }, *tags) + +/** Creates copy of this [CborMap] with the specified [content] and [tags].*/ +@JvmName("copyLongMapList") +public fun CborMap.copy(content: Map, tags: List): CborMap = + copy(content, *tags.toULongArray()) +/*END MAP*/ + + +/*START ARRAY*/ +/** Creates copy of this [CborArray] with the specified [content], copying all tags.*/ +public fun CborArray.copy(content: List): CborArray = CborArray(content, *(rawTags.copyOf())) +/** Creates copy of this [CborArray] with the specified [content] and [tags].*/ +public fun CborArray.copy(content: List, vararg tags: ULong): CborArray = CborArray(content, *tags) +/** Creates copy of this [CborArray] with the specified [content] and [tags].*/ +public fun CborArray.copy(content: List, tags: List): CborArray = + copy(content, *tags.toULongArray()) + +/** Creates copy of this [CborArray] with the specified [content], copying all tags.*/ +public fun CborArray.copy(content: Array): CborArray = CborArray(content, *(rawTags.copyOf())) +/** Creates copy of this [CborArray] with the specified [content] and [tags].*/ +public fun CborArray.copy(content: Array, vararg tags: ULong): CborArray = CborArray(content, *tags) +/** Creates copy of this [CborArray] with the specified [content] and [tags].*/ +public fun CborArray.copy(content: Array, tags: List): CborArray = + copy(content, *tags.toULongArray()) +/*END ARRAY*/ diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/DelicateCborApi.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/DelicateCborApi.kt new file mode 100644 index 0000000000..704aafff86 --- /dev/null +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/DelicateCborApi.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2017-2026 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.cbor + +@MustBeDocumented +@Target(AnnotationTarget.PROPERTY) // no direct targets, only argument to @SubclassOptInRequired +@RequiresOptIn(message = "Accessing this property exposes mutable state. Read-Only access is fine, but manipulating it can cause undefined behaviour", level = RequiresOptIn.Level.ERROR) +public annotation class DelicateCborApi diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index 06875c7daf..a1903dc6ca 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -1,4 +1,4 @@ -@file:OptIn(ExperimentalSerializationApi::class, ExperimentalUnsignedTypes::class) +@file:OptIn(ExperimentalSerializationApi::class, ExperimentalUnsignedTypes::class, DelicateCborApi::class) package kotlinx.serialization.cbor.internal @@ -85,7 +85,7 @@ internal object CborNullSerializer : KSerializer, CborSerializer { override fun serialize(encoder: Encoder, value: CborNull) { val cborWriter = encoder.asCborWriter() - cborWriter.encodeElementTags(value.tags) + cborWriter.encodeElementTags(value.rawTags) encoder.encodeNull() } @@ -102,7 +102,7 @@ internal object CborUndefinedSerializer : KSerializer, CborSerial override fun serialize(encoder: Encoder, value: CborUndefined) { val cborWriter = encoder.asCborWriter() - cborWriter.encodeElementTags(value.tags) + cborWriter.encodeElementTags(value.rawTags) cborWriter.encodeUndefined() } @@ -120,7 +120,7 @@ internal object CborIntSerializer : KSerializer, CborSerializer { override fun serialize(encoder: Encoder, value: CborInteger) { val cborWriter = encoder.asCborWriter() - cborWriter.encodeElementTags(value.tags) + cborWriter.encodeElementTags(value.rawTags) when (value.isPositive) { //@formatter:off true -> cborWriter.encodePositive(value.absoluteValue) @@ -142,7 +142,7 @@ internal object CborFloatSerializer : KSerializer, CborSerializer { override fun serialize(encoder: Encoder, value: CborFloat) { val cborWriter = encoder.asCborWriter() - cborWriter.encodeElementTags(value.tags) + cborWriter.encodeElementTags(value.rawTags) encoder.encodeDouble(value.value) } @@ -163,7 +163,7 @@ internal object CborStringSerializer : KSerializer, CborSerializer { override fun serialize(encoder: Encoder, value: CborString) { val cborWriter = encoder.asCborWriter() - cborWriter.encodeElementTags(value.tags) + cborWriter.encodeElementTags(value.rawTags) encoder.encodeString(value.value) } @@ -185,7 +185,7 @@ internal object CborBooleanSerializer : KSerializer, CborSerializer override fun serialize(encoder: Encoder, value: CborBoolean) { val cborWriter = encoder.asCborWriter() - cborWriter.encodeElementTags(value.tags) + cborWriter.encodeElementTags(value.rawTags) encoder.encodeBoolean(value.value) } @@ -207,7 +207,7 @@ internal object CborByteStringSerializer : KSerializer, CborSeri override fun serialize(encoder: Encoder, value: CborByteString) { val cborWriter = encoder.asCborWriter() - cborWriter.encodeElementTags(value.tags) + cborWriter.encodeElementTags(value.rawTags) cborWriter.encodeByteString(value.getBytes()) } @@ -232,7 +232,7 @@ internal object CborMapSerializer : KSerializer, CborSerializer { override fun serialize(encoder: Encoder, value: CborMap) { val cborWriter = encoder.asCborWriter() - cborWriter.encodeElementTags(value.tags) + cborWriter.encodeElementTags(value.rawTags) MapSerializer(CborElementSerializer, CborElementSerializer).serialize(encoder, value) } @@ -256,7 +256,7 @@ internal object CborArraySerializer : KSerializer, CborSerializer { override fun serialize(encoder: Encoder, value: CborArray) { val cborWriter = encoder.asCborWriter() - cborWriter.encodeElementTags(value.tags) + cborWriter.encodeElementTags(value.rawTags) ListSerializer(CborElementSerializer).serialize(encoder, value) } 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 f41b9ea437..94e4cd0e1c 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -132,7 +132,8 @@ internal open class CborReader(override val cbor: Cbor, internal val parser: Cbo return if (deserializer is CborSerializer) { val collectedTags = parser.processTags(tags) ?: EMPTY_TAGS deserializer.deserialize(this).also { value -> - (value as? CborElement)?.tags = collectedTags + @OptIn(DelicateCborApi::class) + (value as? CborElement)?.rawTags = collectedTags } } else if ((decodeByteArrayAsByteString || cbor.configuration.alwaysUseByteString) && deserializer.descriptor == ByteArraySerializer().descriptor @@ -814,7 +815,8 @@ internal class StructuredCborParser(internal val element: CborElement, private v if (layer.current !is CborByteString) { throw CborDecodingException("Expected byte string, got ${layer.current::class.simpleName}") } - return (layer.current as CborByteString).toByteArray() //do we want to copy here? + @OptIn(DelicateCborApi::class) + return (layer.current as CborByteString).bytes //should be safe not to copy here, right?! } override fun nextDouble(tags: ULongArray?): Double { @@ -858,7 +860,8 @@ internal class StructuredCborParser(internal val element: CborElement, private v // if we're at a primitive, we only process tags // Store collected tags for verification - val collectedTags = if (layer.current.tags.isEmpty()) null else layer.current.tags + @OptIn(DelicateCborApi::class) + val collectedTags = if (layer.current.rawTags.isEmpty()) null else layer.current.rawTags // Verify tags if needed if (verifyObjectTags) { 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 1c088acae2..72c8db9286 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -311,7 +311,10 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { tags = tags ) - is Root -> elements.first().also { it.tags += tags } + is Root -> elements.first().also { + @OptIn(DelicateCborApi::class) + it.rawTags += tags + } } } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDelegatedPropertiesTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDelegatedPropertiesTest.kt index 204a14fdb1..ff0e703032 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDelegatedPropertiesTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDelegatedPropertiesTest.kt @@ -61,7 +61,8 @@ class CborDelegatedPropertiesTest { keyTags: ULongArray = ulongArrayOf(), valueTags: ULongArray = ulongArrayOf(), ) { - content[CborString(key, *keyTags)] = value.also { it.tags += valueTags } + @OptIn(DelicateCborApi::class) + content[CborString(key, *keyTags)] = value.also { it.rawTags += valueTags } } fun remove(key: String, keyTags: ULongArray = ulongArrayOf()) { @@ -84,7 +85,8 @@ class CborDelegatedPropertiesTest { } content[mappedKey] = value } - return MapBackedPerson(content, backing = CborMap(content, *map.tags)) + @OptIn(DelicateCborApi::class) + return MapBackedPerson(content, backing = CborMap(content, *map.rawTags)) } } @@ -186,7 +188,8 @@ class CborDelegatedPropertiesTest { out[if (label == null) key else CborInteger(label)] = v } - cborEncoder.encodeCborElement(CborMap(out, *value.backing.tags)) + @OptIn(DelicateCborApi::class) + cborEncoder.encodeCborElement(CborMap(out, *value.backing.rawTags)) } override fun deserialize(decoder: Decoder): MapBackedPerson { diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt index ac3e938f80..c6deca55a0 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalSerializationApi::class) + package kotlinx.serialization.cbor import kotlinx.serialization.* @@ -8,16 +10,14 @@ class CborElementTest { private val cbor = Cbor {} - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testEncodeToCborElementRootPrimitiveInt() { val element = cbor.encodeToCborElement(42) assertEquals(CborInteger(42), element) assertEquals(42, cbor.decodeFromCborElement(element)) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testEncodeToCborElementRootPrimitiveByteArrayAlwaysUseByteString() { val configured = Cbor { alwaysUseByteString = true } val element = configured.encodeToCborElement(byteArrayOf(1, 2, 3)) @@ -32,16 +32,14 @@ class CborElementTest { @Serializable private data class Wrapper(val datum: Wrapped?) - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testEncodeDecodeNullableClassViaCborElement() { val wrapper = Wrapper(null) val element = cbor.encodeToCborElement(wrapper) assertEquals(wrapper, cbor.decodeFromCborElement(element)) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testEncodeDecodeRootListViaCborElement() { val value = listOf(1, 2, 3) val element = cbor.encodeToCborElement(value) @@ -614,8 +612,7 @@ class CborElementTest { } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testCborElementWithValueTagsFails() { val cbor = Cbor { encodeValueTags = true } val message = assertFailsWith { @@ -635,8 +632,7 @@ class CborElementTest { ) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testCborElementWithKeyTagsFails() { val cbor = Cbor { encodeKeyTags = true } val message = assertFailsWith { @@ -656,8 +652,7 @@ class CborElementTest { ) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testConcreteCborElementWithValueTagsFails() { val cbor = Cbor { encodeValueTags = true } val message = assertFailsWith { @@ -669,8 +664,7 @@ class CborElementTest { ) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testTaggedCborElementPropertyDecodingFails() { val hex = cbor.encodeToHexString(CborMap(mapOf(CborString("cborElement") to CborBoolean(false)))) val message = assertFailsWith { @@ -682,8 +676,7 @@ class CborElementTest { ) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testKeyTaggedCborElementPropertyDecodingFails() { val hex = cbor.encodeToHexString(CborMap(mapOf(CborString("cborElement") to CborBoolean(false)))) val message = assertFailsWith { @@ -695,8 +688,7 @@ class CborElementTest { ) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testConcreteCborElementPropertyDecodingFails() { val hex = cbor.encodeToHexString(CborMap(mapOf(CborString("cborElement") to CborInteger(1)))) val message = assertFailsWith { @@ -708,8 +700,7 @@ class CborElementTest { ) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testCborElementTagsRemainAllowed() { val cbor = Cbor { encodeValueTags = true } val element = CborBoolean(false, 2337u) @@ -718,8 +709,7 @@ class CborElementTest { assertEquals(obj, cbor.decodeFromHexString(MixedUntaggedElement.serializer(), hex)) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testTaggedCborMapKeyRemainsAllowed() { val cbor = Cbor { encodeKeyTags = true } val element = CborMap(mapOf(CborString("key", 42u) to CborBoolean(true))) @@ -759,8 +749,7 @@ class CborElementTest { assertEquals(element, cbor.decodeFromByteArray(CborElement.serializer(), bytes)) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testRootCborElementTagsRespectEncodeValueTags() { val element = CborBoolean(false, 1u) @@ -768,8 +757,7 @@ class CborElementTest { assertEquals("c1f4", Cbor { encodeValueTags = true }.encodeToHexString(CborElement.serializer(), element)) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testArrayCborElementTagsRespectEncodeValueTags() { val element = CborArray(listOf(CborBoolean(false, 1u))) @@ -780,8 +768,7 @@ class CborElementTest { ) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testMapCborElementTagsRespectKeyAndValueSwitches() { val element = CborMap(mapOf(CborString("k", 1u) to CborBoolean(false, 2u))) @@ -803,8 +790,7 @@ class CborElementTest { ) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testNestedCborElementTagsRespectLocalPosition() { val element = CborMap( mapOf( @@ -832,8 +818,7 @@ class CborElementTest { ) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testCborMapTagsRespectPositionWhenUsedAsMapKey() { val element = CborMap(mapOf(CborMap(emptyMap(), 1u) to CborBoolean(true, 2u))) @@ -855,8 +840,7 @@ class CborElementTest { ) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testEncodeToCborElementRespectsRawElementTagSwitches() { val element = CborMap(mapOf(CborString("k", 1u) to CborBoolean(false, 2u)), 3u) @@ -881,8 +865,7 @@ class CborElementTest { ) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testAllRootCborElementKindsRespectEncodeValueTags() { val elements = listOf( CborNull(1u) to CborNull(), @@ -912,8 +895,7 @@ class CborElementTest { } } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testAllRawCborElementMapKeyKindsRespectEncodeKeyTags() { val taggedKeys = listOf( CborString("text", 1u) to CborString("text"), @@ -939,8 +921,7 @@ class CborElementTest { ) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testGenericSerializableRawCborElementRespectsNestedTagSwitches() { val element = CborMap(mapOf(CborString("k", 1u) to CborBoolean(false, 2u)), 3u) val box = GenericBox(element) @@ -966,8 +947,7 @@ class CborElementTest { ) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testGenericSerializableRawCborElementListAndMapRespectTagSwitches() { val listBox = GenericListBox(listOf(CborString("v", 1u))) val listSerializer = GenericListBox.serializer(CborElement.serializer()) @@ -1000,8 +980,7 @@ class CborElementTest { ) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testGenericSerializableConcreteCborElementSubtypesRespectTagSwitches() { val mapBox = GenericBox(CborMap(mapOf(CborString("k", 1u) to CborString("v", 2u)), 3u)) val mapSerializer = GenericBox.serializer(CborMap.serializer()) @@ -1051,8 +1030,7 @@ class CborElementTest { ) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testCborLabelOnCborElementPropertyFails() { val box = LabelledRawElementBox(CborString("x")) val cbor = Cbor { @@ -1086,8 +1064,7 @@ class CborElementTest { ) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testCborLabelOnConcreteCborElementPropertyFails() { val message = assertFailsWith { cbor.encodeToByteArray(LabelledRawIntegerBox.serializer(), LabelledRawIntegerBox(CborInteger(1))) @@ -1098,8 +1075,7 @@ class CborElementTest { ) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testRawCborMapNumericKeyRemainsAllowed() { val element = CborMap(mapOf(CborInteger(1) to CborString("x"))) val encoded = cbor.encodeToByteArray(CborElement.serializer(), element) @@ -1110,8 +1086,7 @@ class CborElementTest { ) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testCborLabelDoesNotMakeTaggedCborElementPropertyAllowed() { val valueMessage = assertFailsWith { cbor.encodeToByteArray(LabelledValueTaggedElement.serializer(), LabelledValueTaggedElement(CborBoolean(false))) @@ -1130,8 +1105,7 @@ class CborElementTest { ) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testDecodingRawCborElementPreservesTagsIndependentOfEncodeSwitches() { assertEquals(CborBoolean(false, 1u), cbor.decodeFromHexString(CborElement.serializer(), "c1f4")) assertEquals( @@ -1140,8 +1114,7 @@ class CborElementTest { ) } - @OptIn(ExperimentalSerializationApi::class) - @Test + @Test fun testConcreteCborElementSerializersRespectRawTagSwitches() { val string = CborString("v", 1u) assertEquals(CborString("v"), cbor.decodeFromByteArray(CborString.serializer(), cbor.encodeToByteArray(string))) From d00559a2b7845f01f9fa4cdb437bd2843acda4de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Tue, 26 May 2026 11:07:45 +0200 Subject: [PATCH 68/68] CBOR: Rename writer/parser class hierarchies --- .../src/kotlinx/serialization/cbor/Cbor.kt | 2 +- .../cbor/internal/CborElementSerializers.kt | 2 +- .../{CborParserInterface.kt => CborParser.kt} | 2 +- .../cbor/internal/CborTreeReader.kt | 2 +- .../{CborWriterInterface.kt => CborWriter.kt} | 2 +- .../serialization/cbor/internal/Decoder.kt | 36 ++++++++++++------- .../serialization/cbor/internal/Encoder.kt | 16 ++++----- .../serialization/cbor/internal/Unsigned.kt | 5 ++- .../serialization/cbor/CborParserTest.kt | 26 +++++++------- 9 files changed, 51 insertions(+), 42 deletions(-) rename formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/{CborParserInterface.kt => CborParser.kt} (96%) rename formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/{CborWriterInterface.kt => CborWriter.kt} (94%) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt index e2aa5135b8..7d8c135884 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt @@ -86,7 +86,7 @@ public sealed class Cbor( override fun decodeFromByteArray(deserializer: DeserializationStrategy, bytes: ByteArray): T { val stream = ByteArrayInput(bytes) - val reader = CborReader(this, CborParser(stream, configuration.verifyObjectTags)) + val reader = CborReader(this, CborParserImpl(stream, configuration.verifyObjectTags)) val result = reader.decodeSerializableValue(deserializer) if (stream.availableBytes > 0) { throw CborDecodingException( diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt index a1903dc6ca..8bc0513b1b 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -276,7 +276,7 @@ internal fun Decoder.asCborDecoder(): CborDecoder = this as? CborDecoder /*need to expose writer to access raw CBOR token operations*/ @IgnorableReturnValue -internal fun Encoder.asCborWriter() = this as? CborWriterInterface +internal fun Encoder.asCborWriter() = this as? CborWriter ?: throw IllegalStateException( "This serializer can be used only with Cbor format. " + "Expected Encoder to provide a CborWriterInterface, got ${this::class}" diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParser.kt similarity index 96% rename from formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt rename to formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParser.kt index c40bbd1ab4..3397b457bd 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParserInterface.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParser.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.* /** * Common interface for CBOR parsers that can read CBOR data from different sources. */ -internal sealed interface CborParserInterface { +internal sealed interface CborParser { // Basic state checks fun isNull(): Boolean fun isEnd(): Boolean diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt index afc7381f91..d06fde8d51 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt @@ -13,7 +13,7 @@ internal class CborTreeReader( //we cannot validate tags, or disregard nulls, can we?! //still, this needs to go here, in case it evolves to a point where we need to respect certain config values private val configuration: CborConfiguration, - private val parser: CborParser + private val parser: CborParserImpl ) { /** * Reads the next CBOR element from the parser. diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborWriterInterface.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborWriter.kt similarity index 94% rename from formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborWriterInterface.kt rename to formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborWriter.kt index 059f15a3d9..9186276f0f 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborWriterInterface.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborWriter.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.* /** * Common interface for CBOR writers that can emit CBOR data to different destinations. */ -internal sealed interface CborWriterInterface { +internal sealed interface CborWriter { // Collection operations are represented by Encoder.beginStructure/endStructure. // Value writing operations 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 94e4cd0e1c..386fdf071e 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -12,12 +12,12 @@ import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* import kotlinx.serialization.modules.* -internal open class CborReader(override val cbor: Cbor, internal val parser: CborParserInterface) : AbstractDecoder(), +internal open class CborReader(override val cbor: Cbor, internal val parser: CborParser) : AbstractDecoder(), CborDecoder { override fun decodeCborElement(): CborElement = when (parser) { - is CborParser -> CborTreeReader(cbor.configuration, parser).read() + is CborParserImpl -> CborTreeReader(cbor.configuration, parser).read() is StructuredCborParser -> parser.layer.current } @@ -47,7 +47,7 @@ internal open class CborReader(override val cbor: Cbor, internal val parser: Cbo "kotlin.UShort", "kotlin.UInt", "kotlin.ULong", - -> UnsignedInlineDecoder(this) + -> UnsignedInlineDecoder(this) else -> super.decodeInline(descriptor) } } @@ -164,9 +164,12 @@ internal open class CborReader(override val cbor: Cbor, internal val parser: Cbo } override fun decodeByte() = nextNumberWithinRange(Byte.MIN_VALUE.toLong(), Byte.MAX_VALUE.toLong(), "Byte").toByte() - override fun decodeShort() = nextNumberWithinRange(Short.MIN_VALUE.toLong(), Short.MAX_VALUE.toLong(), "Short").toShort() + override fun decodeShort() = + nextNumberWithinRange(Short.MIN_VALUE.toLong(), Short.MAX_VALUE.toLong(), "Short").toShort() + override fun decodeChar() = nextNumberWithinRange(Char.MIN_VALUE.code.toLong(), Char.MAX_VALUE.code.toLong(), "Char").toInt().toChar() + override fun decodeInt() = nextNumberWithinRange(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong(), "Int").toInt() override fun decodeLong() = parser.nextNumber(tags) @@ -187,8 +190,7 @@ internal open class CborReader(override val cbor: Cbor, internal val parser: Cbo } -internal class CborParser(private val input: ByteArrayInput, private val verifyObjectTags: Boolean) : - CborParserInterface { +internal class CborParserImpl(private val input: ByteArrayInput, private val verifyObjectTags: Boolean) : CborParser { private var curByteOrEof: Int = -1 internal val curByte: Int @@ -596,16 +598,23 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO return when (majorType) { HEADER_BYTE_STRING, HEADER_STRING, HEADER_ARRAY -> - readUnsignedIntegerIgnoringMajorType { "${majorType.majorTypeName} length" }.asSizedElementLength(majorType) + readUnsignedIntegerIgnoringMajorType { "${majorType.majorTypeName} length" }.asSizedElementLength( + majorType + ) + HEADER_MAP -> - readUnsignedIntegerIgnoringMajorType { "map length" }.asSizedElementLength(majorType, Int.MAX_VALUE / 2) * 2 + readUnsignedIntegerIgnoringMajorType { "map length" }.asSizedElementLength( + majorType, + Int.MAX_VALUE / 2 + ) * 2 + else -> when (additionalInformation) { 24 -> 1 25 -> 2 26 -> 4 27 -> 8 else -> 0 - } + } } } @@ -719,11 +728,11 @@ internal class PeekingIterator private constructor( } /** - * CBOR parser that operates on [CborElement] instead of bytes. Closely mirrors the behaviour of [CborParser], so the + * CBOR parser that operates on [CborElement] instead of bytes. Closely mirrors the behaviour of [CborParserImpl], so the * [CborDecoder] can remain largely unchanged. */ internal class StructuredCborParser(internal val element: CborElement, private val verifyObjectTags: Boolean) : - CborParserInterface { + CborParser { internal var layer: PeekingIterator = PeekingIterator(element) private set @@ -841,6 +850,7 @@ internal class StructuredCborParser(internal val element: CborElement, private v key.longOrNull ?: throw CborDecodingException("$key cannot be represented as Long"), tags ) + else -> throw CborDecodingException("Expected string or number key, got ${key::class.simpleName}") } } @@ -888,13 +898,13 @@ internal class StructuredCborParser(internal val element: CborElement, private v } -private class CborMapReader(cbor: Cbor, decoder: CborParserInterface) : CborArrayReader(cbor, decoder) { +private class CborMapReader(cbor: Cbor, decoder: CborParser) : CborArrayReader(cbor, decoder) { override fun skipBeginToken(objectTags: ULongArray?) = setSize(parser.startMap(tags?.let { if (objectTags == null) it else ulongArrayOf(*it, *objectTags) } ?: objectTags) * 2) } -private open class CborArrayReader(cbor: Cbor, decoder: CborParserInterface) : CborReader(cbor, decoder) { +private open class CborArrayReader(cbor: Cbor, decoder: CborParser) : CborReader(cbor, decoder) { private var ind = 0 override fun skipBeginToken(objectTags: ULongArray?) = 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 72c8db9286..48aa9a30e3 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -15,10 +15,10 @@ import kotlin.experimental.* //value classes are only inlined on the JVM, so we use a typealias and extensions instead -private typealias Stack = MutableList +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(initial: AbstractCborWriter.Data): Stack = mutableListOf(initial) +private fun Stack.push(value: AbstractCborWriter.Data) = add(value) private fun Stack.pop() = removeLast() private fun Stack.peek() = last() @@ -26,9 +26,9 @@ private enum class RawElementTagPosition { KEY, VALUE } // Writes class as map [fieldName, fieldValue] // Split implementation to optimize base case -internal sealed class CborWriter( +internal sealed class AbstractCborWriter( override val cbor: Cbor, -) : AbstractEncoder(), CborEncoder, CborWriterInterface { +) : AbstractEncoder(), CborEncoder, CborWriter { private var tagsMustBeFollowedByDataItem: Boolean = false private var rawElementTagPosition: RawElementTagPosition = RawElementTagPosition.VALUE @@ -232,7 +232,7 @@ internal sealed class CborWriter( } // optimized indefinite length encoder -internal class IndefiniteLengthCborWriter(cbor: Cbor, private val output: ByteArrayOutput) : CborWriter( +internal class IndefiniteLengthCborWriter(cbor: Cbor, private val output: ByteArrayOutput) : AbstractCborWriter( cbor ) { @@ -269,7 +269,7 @@ internal class IndefiniteLengthCborWriter(cbor: Cbor, private val output: ByteAr } // optimized indefinite length encoder -internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { +internal class StructuredCborWriter(cbor: Cbor) : AbstractCborWriter(cbor) { /** * Tags and values are "written", i.e. recorded/prepared for encoding separately. Hence, we need a helper that allows @@ -485,7 +485,7 @@ internal class StructuredCborWriter(cbor: Cbor) : CborWriter(cbor) { } //optimized definite length encoder -internal class DefiniteLengthCborWriter(cbor: Cbor, output: ByteArrayOutput) : CborWriter(cbor) { +internal class DefiniteLengthCborWriter(cbor: Cbor, output: ByteArrayOutput) : AbstractCborWriter(cbor) { private val structureStack = Stack(Data(output, -1)) override fun getDestination(): ByteArrayOutput = diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Unsigned.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Unsigned.kt index 2a14dc0914..a57e491012 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Unsigned.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Unsigned.kt @@ -4,12 +4,11 @@ package kotlinx.serialization.cbor.internal import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.cbor.CborInteger -import kotlinx.serialization.cbor.longOrNull import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder internal class UnsignedInlineEncoder( - private val delegate: CborWriter, + private val delegate: AbstractCborWriter, ) : Encoder by delegate { override fun encodeByte(value: Byte) { delegate.encodePositive(value.toUByte().toULong()) @@ -75,7 +74,7 @@ internal class UnsignedInlineDecoder( integer.absoluteValue.toLong() } - is CborParser -> { + is CborParserImpl -> { parser.processTags(tags) val header = parser.curByte if ((header and MAJOR_TYPE_MASK) != HEADER_POSITIVE.toInt()) { diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborParserTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborParserTest.kt index 1a4ffd749f..a03fdb82c9 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborParserTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborParserTest.kt @@ -12,9 +12,9 @@ import kotlin.test.* class CborParserTest { - private fun withParser(input: String, block: CborParser.() -> Unit) { + private fun withParser(input: String, block: CborParserImpl.() -> Unit) { val bytes = HexConverter.parseHexBinary(input.uppercase()) - CborParser(ByteArrayInput(bytes), false).block() + CborParserImpl(ByteArrayInput(bytes), false).block() } @Test @@ -48,7 +48,7 @@ class CborParserTest { // See https://datatracker.ietf.org/doc/html/rfc8949#section-3.3 @Test fun testUnsupportedSimpleValueEncodings() { - fun CborParser.tryReadAllSimpleValues() { + fun CborParserImpl.tryReadAllSimpleValues() { assertFailsWith { nextBoolean() } assertFailsWith { nextNull() } assertFailsWith { nextFloat() } @@ -899,28 +899,28 @@ class CborParserTest { } -private fun CborParser.nextNumber(tag: ULong): Long = nextNumber(ulongArrayOf(tag)) +private fun CborParserImpl.nextNumber(tag: ULong): Long = nextNumber(ulongArrayOf(tag)) -private fun CborParser.nextDouble(tag: ULong) = nextDouble(ulongArrayOf(tag)) +private fun CborParserImpl.nextDouble(tag: ULong) = nextDouble(ulongArrayOf(tag)) -private fun CborParser.nextString(tag: ULong) = nextString(ulongArrayOf(tag)) +private fun CborParserImpl.nextString(tag: ULong) = nextString(ulongArrayOf(tag)) -private fun CborParser.startArray(tag: ULong): Int = startArray(ulongArrayOf(tag)) +private fun CborParserImpl.startArray(tag: ULong): Int = startArray(ulongArrayOf(tag)) -private fun CborParser.startMap(tag: ULong) = startMap(ulongArrayOf(tag)) +private fun CborParserImpl.startMap(tag: ULong) = startMap(ulongArrayOf(tag)) -private fun CborParser.skipElement(singleTag: ULong) = skipElement(ulongArrayOf(singleTag)) +private fun CborParserImpl.skipElement(singleTag: ULong) = skipElement(ulongArrayOf(singleTag)) -private fun CborParser.skipElement() = skipElement(null) +private fun CborParserImpl.skipElement() = skipElement(null) -private fun CborParser.expect(expected: String, tag: ULong? = null) { +private fun CborParserImpl.expect(expected: String, tag: ULong? = null) { assertEquals(expected, actual = nextString(tag?.let { ulongArrayOf(it) }), "string") } -private fun CborParser.expectMap(size: Int, tag: ULong? = null) { +private fun CborParserImpl.expectMap(size: Int, tag: ULong? = null) { assertEquals(size, actual = startMap(tag?.let { ulongArrayOf(it) }), "map size") } -private fun CborParser.expectEof() { +private fun CborParserImpl.expectEof() { assertTrue(isEof(), "Expected EOF.") }