diff --git a/benchmark/src/jmh/kotlin/kotlinx/benchmarks/cbor/CborBaseLine.kt b/benchmark/src/jmh/kotlin/kotlinx/benchmarks/cbor/CborBaseLine.kt index 8b71d92b7f..0b05d8f50e 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.encodeToCborElement(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.decodeFromCborElement(KTestOuterMessage.serializer(), baseStruct) + + @Benchmark + fun toStruct() = cbor.encodeToCborElement(KTestOuterMessage.serializer(), baseMessage) + } diff --git a/docs/formats.md b/docs/formats.md index ec4e6cb820..9472ee4902 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) @@ -299,7 +304,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) @@ -308,12 +313,138 @@ 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. +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 + +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=[], bytes=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 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): + + + +```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) = 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 `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 + +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. + [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` + * [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 + of individual bytes) + +* [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 + ## ProtoBuf (experimental) @@ -1673,5 +1804,17 @@ 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 +[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 +[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/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 6b580add00..56d640e541 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; } @@ -18,11 +20,63 @@ 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 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 synthetic class kotlinx/serialization/cbor/CborArray$Impl : kotlinx/serialization/cbor/CborArray { - public fun ()V +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 + 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 { + public final fun serializer ()Lkotlinx/serialization/KSerializer; } public final class kotlinx/serialization/cbor/CborBuilder { @@ -52,6 +106,19 @@ 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 final fun toByteArray ()[B + 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 +135,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,17 +144,72 @@ public final class kotlinx/serialization/cbor/CborDecoder$DefaultImpls { public static fun decodeSerializableValue (Lkotlinx/serialization/cbor/CborDecoder;Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object; } +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 final class kotlinx/serialization/cbor/CborElementKt { + 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 + 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 { + 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 } +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 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 { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +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 final fun getAbsoluteValue-s-VKNKU ()J + public fun hashCode ()I + public final fun isPositive ()Z + public fun toString ()Ljava/lang/String; +} + +public final class kotlinx/serialization/cbor/CborInteger$Companion { + public final fun serializer ()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 +224,104 @@ public final synthetic class kotlinx/serialization/cbor/CborLabel$Impl : kotlinx public final synthetic fun label ()J } +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 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 + 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/CborNull : kotlinx/serialization/cbor/CborPrimitive { + public static final field Companion Lkotlinx/serialization/cbor/CborNull$Companion; + public synthetic fun ([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/CborNull$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +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 + public synthetic fun ([JLkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class kotlinx/serialization/cbor/CborPrimitive$Companion { + 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 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 { + 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 @@ -122,6 +343,18 @@ 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 fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +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 } diff --git a/formats/cbor/api/kotlinx-serialization-cbor.klib.api b/formats/cbor/api/kotlinx-serialization-cbor.klib.api index 346416c8fa..9e98d3e192 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] @@ -21,6 +17,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/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] @@ -45,11 +45,58 @@ 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] 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/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] + + 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] + } } final class kotlinx.serialization.cbor/CborBuilder { // kotlinx.serialization.cbor/CborBuilder|null[0] @@ -91,6 +138,19 @@ 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 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] + 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 +178,119 @@ 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] + 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 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] + } +} + +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 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/CborInteger.Companion|null[0] + final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.cbor/CborInteger.Companion.serializer|serializer(){}[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(kotlin/Int): kotlinx.serialization.cbor/CborElement? // kotlinx.serialization.cbor/CborMap.get|get(kotlin.Int){}[0] + final fun get(kotlin/Long): kotlinx.serialization.cbor/CborElement? // kotlinx.serialization.cbor/CborMap.get|get(kotlin.Long){}[0] + final fun get(kotlin/String): kotlinx.serialization.cbor/CborElement? // kotlinx.serialization.cbor/CborMap.get|get(kotlin.String){}[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 getValue(kotlin/Int): kotlinx.serialization.cbor/CborElement // kotlinx.serialization.cbor/CborMap.getValue|getValue(kotlin.Int){}[0] + final fun getValue(kotlin/Long): kotlinx.serialization.cbor/CborElement // kotlinx.serialization.cbor/CborMap.getValue|getValue(kotlin.Long){}[0] + final fun getValue(kotlin/String): kotlinx.serialization.cbor/CborElement // kotlinx.serialization.cbor/CborMap.getValue|getValue(kotlin.String){}[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/CborNull : kotlinx.serialization.cbor/CborPrimitive { // kotlinx.serialization.cbor/CborNull|null[0] + constructor (kotlin/ULongArray...) // kotlinx.serialization.cbor/CborNull.|(kotlin.ULongArray...){}[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] + } +} + +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 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] + } +} + +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 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] + } +} + 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 +300,24 @@ 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] + } +} + +sealed class kotlinx.serialization.cbor/CborPrimitive : kotlinx.serialization.cbor/CborElement { // kotlinx.serialization.cbor/CborPrimitive|null[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] @@ -170,4 +355,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.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/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/Cbor.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt index d922044847..7d8c135884 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() @@ -85,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( @@ -94,7 +95,50 @@ public sealed class Cbor( } return result } + + /** + * 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) + } + + /** + * 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) + writer.ensureNoDanglingTagsAtEndOfSerialization() + 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.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 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 +public inline fun Cbor.decodeFromCborElement(element: CborElement): T = + decodeFromCborElement(serializersModule.serializer(), element) @OptIn(ExperimentalSerializationApi::class) private class CborImpl( @@ -114,18 +158,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/CborDecoder.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborDecoder.kt index 13a773f3fa..3a3de2279c 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][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. + */ + 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..13960ac6d5 --- /dev/null +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborElement.kt @@ -0,0 +1,608 @@ +@file:Suppress("unused") +@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) + +/** + * Class representing single CBOR element. + * 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 + * which has meaningful schemaless semantics only for CBOR. + * + * The whole hierarchy is [serializable][Serializable] only by [Cbor] format. + */ +@Serializable(with = CborElementSerializer::class) +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) + 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). + */ + 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 (!rawTags.contentEquals(other.rawTags)) return false + + return true + } + + override fun hashCode(): Int { + return rawTags.contentHashCode() + } + +} + +/** + * 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( + tags: ULongArray = EMPTY_TAGS +) : CborElement(tags) + +/** + * Class representing either: + * * 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 [isPositive]. Note that [absoluteValue] **must not be** `0` when [isPositive] is set to `false`. + */ +@Serializable(with = CborIntSerializer::class) +public class CborInteger( + public val absoluteValue: ULong, + public val isPositive: Boolean, + vararg tags: ULong +) : CborPrimitive(tags) { + init { + if (!isPositive) require(absoluteValue > 0uL) { "Illegal absolute value $absoluteValue for a negative number." } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + 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 + } + + 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=${rawTags.joinToString(prefix = "[", postfix = "]")}, " + + "absoluteValue=" + when (isPositive) { + true -> "" + false -> "-" + } + + absoluteValue + + ")" + } +} + +/** + * 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)`. + */ +@Suppress("FunctionName") +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) + +/** + * Creates an unsigned CBOR integer (major type 0). + */ +@Suppress("FunctionName") +public fun CborInteger(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]. + */ +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]. + */ +public val CborInteger.longOrNull: Long? + get() { + val max = Long.MAX_VALUE.toULong() + return if (isPositive) { + if (absoluteValue <= max) absoluteValue.toLong() else null + } else { + when { + absoluteValue <= max -> -absoluteValue.toLong() + absoluteValue == max + 1uL -> Long.MIN_VALUE + else -> null + } + } + } + +/** + * Converts this integer to [Int], throwing if it cannot be represented as [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]. + */ +public val CborInteger.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]. + */ +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]. + */ +public val CborInteger.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]. + */ +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]. + */ +public val CborInteger.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). + */ +@Serializable(with = CborFloatSerializer::class) +public class CborFloat( + public val value: Double, + vararg tags: ULong +) : 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=${rawTags.joinToString(prefix = "[", postfix = "]")}, " + + "value=$value" + + ")" + } +} + +/** + * Class representing CBOR string value. + */ +@Serializable(with = CborStringSerializer::class) +public class CborString( + public val value: String, + vararg tags: ULong +) : 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=${rawTags.joinToString(prefix = "[", postfix = "]")}, " + + "value=$value" + + ")" + } +} + +/** + * Class representing CBOR boolean value. + */ +@Serializable(with = CborBooleanSerializer::class) +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 + 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=${rawTags.joinToString(prefix = "[", postfix = "]")}, " + + "value=$value" + + ")" + } +} + +/** + * Class representing CBOR byte string value. + */ +@Serializable(with = CborByteStringSerializer::class) +public class CborByteString( + 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 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 (!rawTags.contentEquals(other.rawTags)) return false + return bytes.contentEquals(other.bytes) + } + + override fun hashCode(): Int { + var result = rawTags.contentHashCode() + result = 31 * result + (bytes.contentHashCode()) + return result + } + + override fun toString(): String { + return "CborByteString(" + + "tags=${rawTags.joinToString(prefix = "[", postfix = "]")}, " + + "bytes=h'${bytes.toHexString()}" + + ")" + } + + internal fun getBytes(): ByteArray = bytes +} + +/** + * Class representing CBOR `null` value + */ +@Serializable(with = CborNullSerializer::class) +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 + return super.equals(other) + } + + override fun hashCode(): Int = CborNull::class.hashCode() * 31 + super.hashCode() + + override fun toString(): String { + return "CborNull(tags=${rawTags.joinToString(prefix = "[", postfix = "]")})" + } +} + +/** + * Class representing CBOR `undefined` value + */ +@Serializable(with = CborUndefinedSerializer::class) +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 + return super.equals(other) + } + + override fun hashCode(): Int = CborUndefined::class.hashCode() * 31 + super.hashCode() + + override fun toString(): String { + return "CborUndefined(tags=${rawTags.joinToString(prefix = "[", postfix = "]")})" + } +} + +/** + * 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, + 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.rawTags.contentEquals(rawTags) + + public override fun hashCode(): Int = content.hashCode() * 31 + rawTags.contentHashCode() + + override fun toString(): String { + return "CborMap(" + + "tags=${rawTags.joinToString(prefix = "[", postfix = "]")}, " + + "content=$content" + + ")" + } + + 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[CborInteger(key)] + public fun getValue(key: Long): CborElement = content.getValue(CborInteger(key)) + + 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) + } + +} + +/** + * 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 = CborArraySerializer::class) +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.rawTags.contentEquals(rawTags) + + public override fun hashCode(): Int = content.hashCode() * 31 + rawTags.contentHashCode() + + override fun toString(): String { + return "CborArray(" + + "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/CborEncoder.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt index 7cfead426a..c9a5f97172 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,34 @@ 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 = 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) + * // ... + * } + * ``` + */ + public fun encodeCborElement(element: CborElement): Unit = encodeSerializableValue(CborElementSerializer, element) + } 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/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/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/CborElementSerializers.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt new file mode 100644 index 0000000000..8bc0513b1b --- /dev/null +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborElementSerializers.kt @@ -0,0 +1,305 @@ +@file:OptIn(ExperimentalSerializationApi::class, ExperimentalUnsignedTypes::class, DelicateCborApi::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.* + +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, CborSerializer { + 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("CborUndefined", defer { CborUndefinedSerializer.descriptor }) + element("CborString", defer { CborStringSerializer.descriptor }) + element("CborBoolean", defer { CborBooleanSerializer.descriptor }) + element("CborByteString", defer { CborByteStringSerializer.descriptor }) + element("CborMap", defer { CborMapSerializer.descriptor }) + element("CborArray", defer { CborArraySerializer.descriptor }) + element("CborDouble", defer { CborFloatSerializer.descriptor }) + element("CborInt", defer { CborIntSerializer.descriptor }) + } + + override fun serialize(encoder: Encoder, value: CborElement) { + encoder.asCborWriter() + + // Encode the value + when (value) { + is CborPrimitive -> encoder.encodeSerializableValue(CborPrimitiveSerializer, value) + is CborMap -> encoder.encodeSerializableValue(CborMapSerializer, value) + is CborArray -> encoder.encodeSerializableValue(CborArraySerializer, 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, CborSerializer { + override val descriptor: SerialDescriptor = + buildSerialDescriptor("kotlinx.serialization.cbor.CborPrimitive", PolymorphicKind.SEALED) + + 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) + is CborFloat -> encoder.encodeSerializableValue(CborFloatSerializer, value) + is CborInteger -> encoder.encodeSerializableValue(CborIntSerializer, 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, CborSerializer { + + override val descriptor: SerialDescriptor = + buildSerialDescriptor("kotlinx.serialization.cbor.CborNull", SerialKind.ENUM) + + override fun serialize(encoder: Encoder, value: CborNull) { + val cborWriter = encoder.asCborWriter() + cborWriter.encodeElementTags(value.rawTags) + encoder.encodeNull() + } + + override fun deserialize(decoder: Decoder): CborNull { + 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) + + override fun serialize(encoder: Encoder, value: CborUndefined) { + val cborWriter = encoder.asCborWriter() + cborWriter.encodeElementTags(value.rawTags) + cborWriter.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 + } +} + + +internal object CborIntSerializer : KSerializer, CborSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborInt", PrimitiveKind.LONG) + + override fun serialize(encoder: Encoder, value: CborInteger) { + val cborWriter = encoder.asCborWriter() + cborWriter.encodeElementTags(value.rawTags) + when (value.isPositive) { + //@formatter:off + true -> cborWriter.encodePositive(value.absoluteValue) + false -> cborWriter.encodeNegative(value.absoluteValue) + //@formatter:on + } + } + + override fun deserialize(decoder: Decoder): CborInteger { + val result = decoder.asCborDecoder().decodeCborElement() + if (result !is CborInteger) throw CborDecodingException("Unexpected CBOR element, expected CborInt, had ${result::class}") + return result + } +} + +internal object CborFloatSerializer : KSerializer, CborSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborDouble", PrimitiveKind.DOUBLE) + + override fun serialize(encoder: Encoder, value: CborFloat) { + val cborWriter = encoder.asCborWriter() + cborWriter.encodeElementTags(value.rawTags) + encoder.encodeDouble(value.value) + } + + override fun deserialize(decoder: Decoder): CborFloat { + val element = decoder.asCborDecoder().decodeCborElement() + if (element !is CborFloat) throw CborDecodingException("Unexpected CBOR element, expected CborFloat, 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, CborSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborString", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: CborString) { + val cborWriter = encoder.asCborWriter() + cborWriter.encodeElementTags(value.rawTags) + encoder.encodeString(value.value) + } + + override fun deserialize(decoder: Decoder): CborString { + val cborDecoder = decoder.asCborDecoder() + val element = cborDecoder.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, CborSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("kotlinx.serialization.cbor.CborBoolean", PrimitiveKind.BOOLEAN) + + override fun serialize(encoder: Encoder, value: CborBoolean) { + val cborWriter = encoder.asCborWriter() + cborWriter.encodeElementTags(value.rawTags) + encoder.encodeBoolean(value.value) + } + + override fun deserialize(decoder: Decoder): CborBoolean { + val cborDecoder = decoder.asCborDecoder() + val element = cborDecoder.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 and its input ([CborDecoder] and [CborEncoder]). + */ +internal object CborByteStringSerializer : KSerializer, CborSerializer { + override val descriptor: SerialDescriptor = + SerialDescriptor("kotlinx.serialization.cbor.CborByteString", ByteArraySerializer().descriptor) + + override fun serialize(encoder: Encoder, value: CborByteString) { + val cborWriter = encoder.asCborWriter() + cborWriter.encodeElementTags(value.rawTags) + cborWriter.encodeByteString(value.getBytes()) + } + + override fun deserialize(decoder: Decoder): CborByteString { + val cborDecoder = decoder.asCborDecoder() + val element = cborDecoder.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 and its input ([CborDecoder] and [CborEncoder]). + */ +internal object CborMapSerializer : KSerializer, CborSerializer { + override val descriptor: SerialDescriptor = + SerialDescriptor( + serialName = "kotlinx.serialization.cbor.CborMap", + original = MapSerializer(CborElementSerializer, CborElementSerializer).descriptor + ) + + override fun serialize(encoder: Encoder, value: CborMap) { + val cborWriter = encoder.asCborWriter() + cborWriter.encodeElementTags(value.rawTags) + MapSerializer(CborElementSerializer, CborElementSerializer).serialize(encoder, value) + } + + override fun deserialize(decoder: Decoder): CborMap { + val element = decoder.asCborDecoder().decodeCborElement() + if (element !is CborMap) throw CborDecodingException("Unexpected CBOR element, expected CborMap, had ${element::class}") + return element + } +} + +/** + * 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 CborArraySerializer : KSerializer, CborSerializer { + override val descriptor: SerialDescriptor = + SerialDescriptor( + serialName = "kotlinx.serialization.cbor.CborArray", + original = ListSerializer(CborElementSerializer).descriptor + ) + + override fun serialize(encoder: Encoder, value: CborArray) { + val cborWriter = encoder.asCborWriter() + cborWriter.encodeElementTags(value.rawTags) + ListSerializer(CborElementSerializer).serialize(encoder, value) + } + + override fun deserialize(decoder: Decoder): CborArray { + val element = decoder.asCborDecoder().decodeCborElement() + if (element !is CborArray) throw CborDecodingException("Unexpected CBOR element, expected CborArray, had ${element::class}") + return element + } +} + + +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}" + ) + +/*need to expose writer to access raw CBOR token operations*/ +@IgnorableReturnValue +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}" + ) + +/** + * 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/CborParser.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParser.kt new file mode 100644 index 0000000000..3397b457bd --- /dev/null +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborParser.kt @@ -0,0 +1,41 @@ +@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 sealed interface CborParser { + // 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 + //used only to skip unknown elements + fun skipElement(tags: ULongArray?) + + // Tag verification + fun verifyTagsAndThrow(expected: ULongArray, actual: ULongArray?) + + @IgnorableReturnValue + fun processTags(tags: ULongArray?): ULongArray? +} 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..d06fde8d51 --- /dev/null +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborTreeReader.kt @@ -0,0 +1,145 @@ +@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( + //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: CborParserImpl +) { + /** + * Reads the next CBOR element from the parser. + */ + fun read(): CborElement { + // 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.nextULong() + CborInteger(value, isPositive = true, tags = tags) + } + + 1 -> { // Major type 1: negative integer + val value = parser.nextULong() + 1uL + CborInteger(value, isPositive = false, tags = tags) + } + + 2 -> { // Major type 2: byte string + CborByteString(parser.nextByteString(), tags = tags) + } + + 3 -> { // Major type 3: text string + CborString(parser.nextString(), tags = tags) + } + + 4 -> { // Major type 4: array + readArray(tags) + } + + 5 -> { // Major type 5: map + readMap(tags) + } + + 7 -> { // Major type 7: simple/float/break + when (parser.curByte) { + 0xF4 -> { + CborBoolean(parser.nextBoolean(null), tags = tags) + } + + 0xF5 -> { + CborBoolean(parser.nextBoolean(null), tags = tags) + } + + 0xF6 -> { + parser.nextNull(null) + CborNull(tags = tags) + } + + 0xF7 -> { + parser.skipElement(null) + CborUndefined(tags = tags) + } + // Half/Float32/Float64 + 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()}" + ) + } + } + + else -> { + val errByte = parser.curByte shr 5 + throw if (errByte == -1) CborDecodingException("Unexpected EOF") + else CborDecodingException("Invalid CBOR major type: $errByte") + } + } + return result + } + + /** + * Reads any tags preceding the current value. + * @return An array of tags, possibly empty + */ + private fun readTags(): ULongArray { + if ((parser.curByte shr 5) != 6) return EMPTY_TAGS + + val tags = mutableListOf() + while ((parser.curByte shr 5) == 6) { // Major type 6: tag + tags.add(parser.nextTag()) + } + return tags.toULongArray() + } + + private fun readArray(tags: ULongArray): CborArray { + val size = parser.startArray(null) + 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 CborArray(elements, tags = tags) + } + + private fun readMap(tags: ULongArray): CborMap { + val size = parser.startMap(null) + 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, tags = tags) + } +} diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborWriter.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborWriter.kt new file mode 100644 index 0000000000..9186276f0f --- /dev/null +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborWriter.kt @@ -0,0 +1,28 @@ +@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 CborWriter { + // 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) + 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 8371f50f8b..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,9 +12,16 @@ 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, internal val parser: CborParser) : AbstractDecoder(), CborDecoder { + override fun decodeCborElement(): CborElement = + when (parser) { + is CborParserImpl -> CborTreeReader(cbor.configuration, parser).read() + is StructuredCborParser -> parser.layer.current + } + + protected var size = -1 private set protected var finiteMode = false @@ -22,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) { @@ -34,14 +41,25 @@ 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) 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) } @@ -51,7 +69,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 { @@ -66,6 +84,7 @@ internal open class CborReader(override val cbor: Cbor, protected val parser: Cb if (index == CompositeDecoder.UNKNOWN_NAME) { parser.skipElement(tags) } else { + descriptor.throwIfCborElementHasIncompatibleAnnotations(index) verifyKeyTags(descriptor, index, tags) knownIndex = index break @@ -77,6 +96,7 @@ internal open class CborReader(override val cbor: Cbor, protected val parser: Cb val (elemName, tags) = decodeElementNameWithTags(descriptor) readProperties++ descriptor.getElementIndexOrThrow(elemName).also { index -> + descriptor.throwIfCborElementHasIncompatibleAnnotations(index) verifyKeyTags(descriptor, index, tags) } } @@ -109,7 +129,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) + return if (deserializer is CborSerializer) { + val collectedTags = parser.processTags(tags) ?: EMPTY_TAGS + deserializer.deserialize(this).also { value -> + @OptIn(DelicateCborApi::class) + (value as? CborElement)?.rawTags = collectedTags + } + } else if ((decodeByteArrayAsByteString || cbor.configuration.alwaysUseByteString) && deserializer.descriptor == ByteArraySerializer().descriptor ) { @Suppress("UNCHECKED_CAST") @@ -129,18 +155,22 @@ 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() + 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) @@ -157,11 +187,15 @@ internal open class CborReader(override val cbor: Cbor, protected val parser: Cb } } } + } -internal class CborParser(private val input: ByteArrayInput, private val verifyObjectTags: Boolean) { +internal class CborParserImpl(private val input: ByteArrayInput, private val verifyObjectTags: Boolean) : CborParser { 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 @@ -185,12 +219,12 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO readByte() } - fun isNull() = with(peekCurByteOrFail()) { this == NULL || this == EMPTY_MAP } + override fun isNull(): Boolean = with(peekCurByteOrFail()) { this == NULL || this == EMPTY_MAP } - fun nextNull(tags: ULongArray? = null): Nothing? { + override fun nextNull(tags: ULongArray?): Nothing? { processTags(tags) if (isNull()) { - /* val _ = */ readByte() + val _ = readByte() return null } throw CborDecodingException( @@ -199,7 +233,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO ) } - fun nextBoolean(tags: ULongArray? = null): Boolean { + override fun nextBoolean(tags: ULongArray?): Boolean { processTags(tags) val ans = when (val byte = peekCurByteOrFail()) { TRUE -> true @@ -210,9 +244,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?): Int = startSized(tags, BEGIN_ARRAY, HEADER_ARRAY, "array") - fun startMap(tags: ULongArray? = null) = 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?, @@ -245,11 +279,13 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO return size } - fun isEnd() = peekCurByteOrFail() == BREAK + override fun isEnd(): Boolean = peekCurByteOrFail() == BREAK - fun end() = skipByte(BREAK) + override fun end() { + skipByte(BREAK) + } - fun nextByteString(tags: ULongArray? = null): ByteArray { + override fun nextByteString(tags: ULongArray?): ByteArray { processTags(tags) val header = peekCurByteOrFail() if ((header and MAJOR_TYPE_MASK) != HEADER_BYTE_STRING) { @@ -268,7 +304,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?): 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 { @@ -296,7 +332,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO } @IgnorableReturnValue - private fun processTags(tags: ULongArray?): ULongArray? { + override fun processTags(tags: ULongArray?): ULongArray? { var index = 0 val collectedTags = mutableListOf() while ((peekCurByteOrFail() and MAJOR_TYPE_MASK) == HEADER_TAG) { @@ -320,15 +356,15 @@ 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") + } } } } } - 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()}" @@ -338,30 +374,22 @@ 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) val majorType = peekCurByteOrFail() - if ((majorType and MAJOR_TYPE_MASK) == HEADER_STRING) { + 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 = readUnsignedIntegerIgnoringMajorType { majorType.majorTypeName } 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])") + Triple(null, res, collectedTags) } - return number } - fun nextNumber(tags: ULongArray? = null): Long { + override fun nextNumber(tags: ULongArray?): Long { processTags(tags) val res = readNumber() readByte() @@ -378,7 +406,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO 25 -> 2 26 -> 4 27 -> 8 - else /* > 27 */ -> throw CborDecodingException( + else -> throw CborDecodingException( "Unexpected value encoding when reading ${valueDescriptionForError()}. " + "Expected addition info value < 28, got $additionalInfo " + "(decoded from ${printByte(peekCurByteOrFail())})" @@ -420,7 +448,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 (val headerByte = peekCurByteOrFail()) { NEXT_FLOAT -> Float.fromBits(readInt()) @@ -431,7 +459,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 (val headerByte = peekCurByteOrFail()) { NEXT_DOUBLE -> Double.fromBits(readLong()) @@ -482,12 +510,15 @@ 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() + if (isEof()) throw CborDecodingException("Unexpected EOF while skipping element") processTags(tags) do { + if (isEof()) throw CborDecodingException("Unexpected EOF while skipping element") + if (isIndefinite()) { lengthStack.add(LENGTH_STACK_INDEFINITE) } else if (isEnd()) { @@ -566,12 +597,17 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO val additionalInformation = curByte and ADDITIONAL_INFO_MASK 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 -> + 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 @@ -619,6 +655,23 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO } 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 @@ -644,14 +697,214 @@ 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` + */ +internal class PeekingIterator private constructor( + internal val isStructure: Boolean, + private val iter: ListIterator +) : Iterator by iter { + + lateinit var current: CborElement + private set + + override fun next(): CborElement = iter.next().also { current = it } + + fun peek() = if (hasNext()) { + val next = iter.next() + val _ = iter.previous() + next + } else null + + companion object { + operator fun invoke(single: CborElement): PeekingIterator = + PeekingIterator(false, listOf(single).listIterator()).also { val _ = it.next() } + + operator fun invoke(iter: ListIterator): PeekingIterator = + PeekingIterator(true, iter) + } +} + +/** + * 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) : + CborParser { + + internal var layer: PeekingIterator = PeekingIterator(element) + private set + + + 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.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() + + override fun end() { + // Reset iterators when ending a structure + layer = layerStack.removeLast() + } + + override fun startArray(tags: ULongArray?): Int { + processTags(tags) + if (layer.current !is CborArray) { + throw CborDecodingException("Expected array, got ${layer.current::class.simpleName}") + } + layerStack += layer + 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 + } + + override fun startMap(tags: ULongArray?): Int { + processTags(tags) + if (layer.current !is CborMap) { + throw CborDecodingException("Expected map, got ${layer.current::class.simpleName}") + } + layerStack += layer + + 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 = 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.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.current !is CborBoolean) { + throw CborDecodingException("Expected boolean, got ${layer.current::class.simpleName}") + } + return (layer.current as CborBoolean).value + } + + override fun nextNumber(tags: ULongArray?): Long { + processTags(tags) + if (layer.current !is CborInteger) { + throw CborDecodingException("Expected number, got ${layer.current::class.simpleName}") + } + return (layer.current as CborInteger).longOrNull + ?: throw CborDecodingException("${layer.current} cannot be represented as Long") + } + + override fun nextString(tags: ULongArray?): String { + processTags(tags) + if (layer.current !is CborString) { + throw CborDecodingException("Expected string, got ${layer.current::class.simpleName}") + } + return (layer.current as CborString).value + } + + override fun nextByteString(tags: ULongArray?): ByteArray { + processTags(tags) + if (layer.current !is CborByteString) { + throw CborDecodingException("Expected byte string, got ${layer.current::class.simpleName}") + } + @OptIn(DelicateCborApi::class) + return (layer.current as CborByteString).bytes //should be safe not to copy here, right?! + } + + override fun nextDouble(tags: ULongArray?): Double { + processTags(tags) + return when (layer.current) { + is CborFloat -> (layer.current as CborFloat).value + else -> throw CborDecodingException("Expected double, got ${layer.current::class.simpleName}") + } + } + + override fun nextFloat(tags: ULongArray?): Float { + return nextDouble(tags).toFloat() + } + + override fun nextTaggedStringOrNumber(): Triple { + val tags = processTags(null) + + return when (val key = layer.current) { + is CborString -> Triple(key.value, null, tags) + is CborInteger -> 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}") + } + } + + /** + * 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 + */ + @IgnorableReturnValue + override fun processTags(tags: ULongArray?): ULongArray? { + + // If we're in a list/map, advance to the next element + if (layer.hasNext()) { + val _ = layer.next() + } + // if we're at a primitive, we only process tags + + // Store collected tags for verification + @OptIn(DelicateCborApi::class) + val collectedTags = if (layer.current.rawTags.isEmpty()) null else layer.current.rawTags + + // 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( + "CBOR tags ${actual?.contentToString()} do not match expected tags ${expected.contentToString()}" + ) + } + } + + override fun skipElement(tags: ULongArray?) { + // Process tags but don't do anything with the element + processTags(tags) + } +} + -private class CborMapReader(cbor: Cbor, decoder: CborParser) : CborListReader(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 CborListReader(cbor: Cbor, decoder: CborParser) : CborReader(cbor, decoder) { +private open class CborArrayReader(cbor: Cbor, decoder: CborParser) : CborReader(cbor, decoder) { private var ind = 0 override fun skipBeginToken(objectTags: ULongArray?) = @@ -666,7 +919,6 @@ private open class CborListReader(cbor: Cbor, decoder: CborParser) : CborReader( } } - private val normalizeBaseBits = SINGLE_PRECISION_NORMALIZE_BASE.toBits() 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..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,20 +15,73 @@ 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() +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, - protected val output: ByteArrayOutput, -) : AbstractEncoder(), CborEncoder { - protected var isClass = false +) : AbstractEncoder(), CborEncoder, CborWriter { + + private var tagsMustBeFollowedByDataItem: Boolean = false + private var rawElementTagPosition: RawElementTagPosition = RawElementTagPosition.VALUE + + 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) + } + + 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) { + onDataItemEncoded() + getDestination().encodeByteString(byteArray) + } + + override fun encodeUndefined() { + onDataItemEncoded() + getDestination().encodeUndefined() + } protected var encodeByteArrayAsByteString = false @@ -46,7 +99,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) @@ -56,55 +109,86 @@ 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) { + 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) } + final override fun encodeRawNumber(value: Long) = encodeLong(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() { - if (isClass) getDestination().encodeEmptyMap() - else getDestination().encodeNull() + onDataItemEncoded() + getDestination().encodeNull() } @OptIn(ExperimentalSerializationApi::class) // KT-46731 @@ -112,12 +196,14 @@ internal sealed class CborWriter( enumDescriptor: SerialDescriptor, index: Int ) { + onDataItemEncoded() getDestination().encodeString(enumDescriptor.getElementName(index)) } override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean { + descriptor.throwIfCborElementHasIncompatibleAnnotations(index) + setRawElementTagPosition(descriptor, index) val destination = getDestination() - isClass = descriptor.getElementDescriptor(index).kind == StructureKind.CLASS encodeByteArrayAsByteString = descriptor.isByteString(index) val name = descriptor.getElementName(index) @@ -145,13 +231,13 @@ 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) : AbstractCborWriter( + cbor ) { override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { + onDataItemEncoded() if (cbor.configuration.encodeObjectTags) descriptor.getObjectTags()?.forEach { output.encodeTag(it) } @@ -168,6 +254,7 @@ internal class IndefiniteLengthCborWriter(cbor: Cbor, output: ByteArrayOutput) : } override fun endStructure(descriptor: SerialDescriptor) { + ensureNoDanglingTags("endStructure") output.end() } @@ -177,10 +264,228 @@ internal class IndefiniteLengthCborWriter(cbor: Cbor, output: ByteArrayOutput) : override fun incrementChildren() {/*NOOP*/ } + override fun encodeTagsImpl(tags: ULongArray) = tags.forEach { getDestination().encodeTag(it) } + +} + +// optimized indefinite length encoder +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 + * for setting tags and values independently, and then merging them into the final [CborElement] at the end. + */ + 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()) : + CborContainer(tags, elements) + + class List(tags: ULongArray, elements: MutableList = mutableListOf()) : + CborContainer(tags, elements) + + class Root(tags: ULongArray) : CborContainer(tags, elements = mutableListOf()) { + override fun add(element: CborElement) { + require(elements.isEmpty()) { "Implementation error. Please report a bug." } + elements.add(element) + } + } + + fun finalize() = when (this) { + is List -> CborArray(content = elements, tags = tags) + is Map -> CborMap( + 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 + ) + + is Root -> elements.first().also { + @OptIn(DelicateCborApi::class) + it.rawTags += tags + } + + } + } + + private operator fun CborContainer.plusAssign(element: CborElement) { + add(element) + } + + private val stack = ArrayDeque() + 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 + 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 { + onDataItemEncoded() + val tags = takePendingValueTags() + + if (cbor.configuration.encodeObjectTags) descriptor.getObjectTags() ?: EMPTY_TAGS + else EMPTY_TAGS + 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) + } + } + stack.addLast(currentElement) + currentElement = element + return this + } + + override fun endStructure(descriptor: SerialDescriptor) { + ensureNoDanglingTags("endStructure") + val finalized = currentElement.finalize() + if (stack.isNotEmpty()) { + currentElement = stack.removeLast() + currentElement += finalized + } + } + + override fun getDestination() = throw IllegalStateException("There is no byteArrayInput") + + override fun incrementChildren() { + /*NOOP*/ + } + + + 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) + if (!descriptor.hasArrayTag()) { + 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 + val cborLabel = descriptor.getCborLabel(index) + if (cbor.configuration.preferCborLabelsOverNames && cborLabel != null) { + currentElement += CborInteger(value = cborLabel, tags = keyTags ?: EMPTY_TAGS) + } else { + currentElement += CborString(name, tags = keyTags ?: EMPTY_TAGS) + } + } + } + + if (cbor.configuration.encodeValueTags) { + descriptor.getValueTags(index).let { valueTags -> + //collect them for late encoding in beginStructure or encodeXXX + pendingValueTags = valueTags ?: EMPTY_TAGS + } + } + return true + } + + + 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 += CborInteger(value.toLong(), tags = takePendingValueTags()) + } + + override fun encodeChar(value: Char) { + onDataItemEncoded() + currentElement += CborInteger(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 += CborInteger(value.toLong(), tags = takePendingValueTags()) + } + + override fun encodeLong(value: Long) { + onDataItemEncoded() + currentElement += CborInteger(value, tags = takePendingValueTags()) + } + + override fun encodeNegative(value: ULong) { + onDataItemEncoded() + currentElement += CborInteger(value, isPositive = false, tags = takePendingValueTags()) + } + + override fun encodePositive(value: ULong) { + onDataItemEncoded() + currentElement += CborInteger(value, isPositive = true, tags = takePendingValueTags()) + } + + + override fun encodeShort(value: Short) { + onDataItemEncoded() + currentElement += CborInteger(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 += CborNull(tags = tags) + } + + override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) { + onDataItemEncoded() + currentElement += CborString(enumDescriptor.getElementName(index), tags = takePendingValueTags()) + } + } //optimized definite length encoder -internal class DefiniteLengthCborWriter(cbor: Cbor, output: ByteArrayOutput) : CborWriter(cbor, output) { +internal class DefiniteLengthCborWriter(cbor: Cbor, output: ByteArrayOutput) : AbstractCborWriter(cbor) { private val structureStack = Stack(Data(output, -1)) override fun getDestination(): ByteArrayOutput = @@ -191,13 +496,17 @@ internal class DefiniteLengthCborWriter(cbor: Cbor, output: ByteArrayOutput) : C structureStack.peek().elementCount++ } + 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() @@ -234,7 +543,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) } @@ -242,6 +551,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) @@ -250,6 +561,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) } @@ -330,3 +644,8 @@ private fun composeNegative(value: Long): ByteArray { 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/commonMain/src/kotlinx/serialization/cbor/internal/Encoding.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoding.kt index 72b67465eb..82c1e3f09c 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 @@ -70,7 +71,7 @@ internal fun SerialDescriptor.getCborLabel(index: Int): Long? = findAnnotation 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/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..a57e491012 --- /dev/null +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Unsigned.kt @@ -0,0 +1,87 @@ +@file:OptIn(ExperimentalSerializationApi::class, ExperimentalUnsignedTypes::class) + +package kotlinx.serialization.cbor.internal + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.cbor.CborInteger +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +internal class UnsignedInlineEncoder( + private val delegate: AbstractCborWriter, +) : 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.absoluteValue.toLong() + } + + is CborParserImpl -> { + 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/CborArrayTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborArrayTest.kt index c0b859566b..8a75f38c95 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) @@ -18,6 +18,10 @@ class CborArrayTest { val cbor = Cbor.CoseCompliant assertEquals(referenceHexString, cbor.encodeToHexString(ClassAs1Array.serializer(), reference)) assertEquals(reference, cbor.decodeFromHexString(ClassAs1Array.serializer(), referenceHexString)) + + val struct = cbor.encodeToCborElement(ClassAs1Array.serializer(), reference) + assertEquals(reference, cbor.decodeFromCborElement(ClassAs1Array.serializer(), struct)) + assertEquals(referenceHexString, cbor.encodeToHexString(CborElement.serializer(), struct)) } @Test @@ -35,6 +39,10 @@ class CborArrayTest { val cbor = Cbor.CoseCompliant assertEquals(referenceHexString, cbor.encodeToHexString(ClassAs2Array.serializer(), reference)) assertEquals(reference, cbor.decodeFromHexString(ClassAs2Array.serializer(), referenceHexString)) + + val struct = cbor.encodeToCborElement(ClassAs2Array.serializer(), reference) + assertEquals(reference, cbor.decodeFromCborElement(ClassAs2Array.serializer(), struct)) + assertEquals(referenceHexString, cbor.encodeToHexString(CborElement.serializer(), struct)) } @Test @@ -45,15 +53,19 @@ 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 assertEquals(referenceHexString, cbor.encodeToHexString(ClassAs4ArrayNullable.serializer(), reference)) assertEquals(reference, cbor.decodeFromHexString(ClassAs4ArrayNullable.serializer(), referenceHexString)) + + val struct = cbor.encodeToCborElement(ClassAs4ArrayNullable.serializer(), reference) + assertEquals(reference, cbor.decodeFromCborElement(ClassAs4ArrayNullable.serializer(), struct)) + assertEquals(referenceHexString, cbor.encodeToHexString(CborElement.serializer(), struct)) } @Test @@ -75,12 +87,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.encodeToCborElement(ClassWithArray.serializer(), reference) + assertEquals(reference, cbor.decodeFromCborElement(ClassWithArray.serializer(), struct)) + assertEquals(referenceHexString, cbor.encodeToHexString(CborElement.serializer(), struct)) } @@ -103,16 +113,22 @@ class CborArrayTest { val cbor = Cbor.CoseCompliant assertEquals(referenceHexString, cbor.encodeToHexString(DoubleTaggedClassWithArray.serializer(), reference)) assertEquals(reference, cbor.decodeFromHexString(DoubleTaggedClassWithArray.serializer(), referenceHexString)) + + val struct = cbor.encodeToCborElement(DoubleTaggedClassWithArray.serializer(), reference) + val structFromHex = cbor.decodeFromHexString(CborElement.serializer(), referenceHexString) + assertEquals(structFromHex, struct) + assertEquals(reference, cbor.decodeFromCborElement(DoubleTaggedClassWithArray.serializer(), struct)) + assertEquals(referenceHexString, cbor.encodeToHexString(CborElement.serializer(), struct)) } - @CborArray + @CborObjectAsArray @Serializable data class ClassAs1Array( @SerialName("alg") val alg: Int, ) - @CborArray + @CborObjectAsArray @ObjectTags(8U) @Serializable data class ClassAs2Array( @@ -122,7 +138,7 @@ class CborArrayTest { val kid: String, ) - @CborArray + @CborObjectAsArray @Serializable data class ClassAs4ArrayNullable( @SerialName("alg") @@ -176,4 +192,3 @@ class CborArrayTest { val array: ClassAs2Array, ) } - diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt index 50658f5ca7..e51c2c9b10 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDecoderTest.kt @@ -17,7 +17,14 @@ class CborDecoderTest { @Test fun testDecodeSimpleObject() { - assertEquals(Simple("str"), Cbor.decodeFromHexString(Simple.serializer(), "bf616163737472ff")) + val hex = "bf616163737472ff" + val reference = Simple("str") + assertEquals(reference, Cbor.decodeFromHexString(Simple.serializer(), hex)) + + val struct = Cbor.decodeFromHexString(hex) + assertEquals(reference, Cbor.decodeFromCborElement(Simple.serializer(), struct)) + + assertEquals(hex, Cbor.encodeToHexString(CborElement.serializer(), struct)) } @Test @@ -33,20 +40,40 @@ 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 = + "bf637374726d48656c6c6f2c20776f726c64216169182a686e756c6c61626c65f6646c6973749f61616162ff636d6170bf01f502f4ff65696e6e6572bf6161636c6f6cff6a696e6e6572734c6973749fbf6161636b656bffff6a62797465537472696e6742cafe696279746541727261799f383521ffff" assertEquals( test, Cbor.decodeFromHexString( TypesUmbrella.serializer(), - "bf637374726d48656c6c6f2c20776f726c64216169182a686e756c6c61626c65f6646c6973749f61616162ff636d6170bf01f502f4ff65696e6e6572bf6161636c6f6cff6a696e6e6572734c6973749fbf6161636b656bffff6a62797465537472696e675f42cafeff696279746541727261799f383521ffff" + hex ) ) + + val struct = Cbor.decodeFromHexString(hex) + 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)) + + + // 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.encodeToCborElement(test), structDef) + assertEquals(test, Cbor.decodeFromCborElement(TypesUmbrella.serializer(), structDef)) + } @Test @@ -57,31 +84,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.decodeFromCborElement(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(hexNull) + assertEquals(expectedNull, Cbor.decodeFromCborElement(NullableByteString.serializer(), structNull)) } @Test fun testNullables() { assertEquals(NullableByteStringDefaultNull(), Cbor.decodeFromHexString("a0")) + val struct = Cbor.decodeFromHexString("a0") + assertEquals(NullableByteStringDefaultNull(), Cbor.decodeFromCborElement(NullableByteStringDefaultNull.serializer(), struct)) } /** @@ -91,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.decodeFromCborElement( + 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.decodeFromCborElement( + Simple.serializer(), + structDef ) } } @@ -121,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" @@ -141,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 @@ -160,19 +231,26 @@ 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) + } } @Test fun testDecodeCborWithUnknownField() { + val hex = "bf616163313233616263393837ff" + val expected = Simple("123") assertEquals( - expected = Simple("123"), + expected = expected, actual = ignoreUnknownKeys.decodeFromHexString( deserializer = Simple.serializer(), @@ -187,15 +265,20 @@ class CborDecoderTest { * 393837 # "987" * FF # primitive(*) */ - hex = "bf616163313233616263393837ff" + hex = hex ) ) + val struct = Cbor.decodeFromHexString(hex) + assertEquals(expected, ignoreUnknownKeys.decodeFromCborElement(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 +308,12 @@ class CborDecoderTest { * FF # primitive(*) * FF # primitive(*) */ - hex = "bf6161633132336162bf7f6178ffa161790aff61639f010203ffff" + hex = hex ) ) + + val struct = Cbor.decodeFromHexString(hex) + assertEquals(expected, ignoreUnknownKeys.decodeFromCborElement(Simple.serializer(), struct)) } /** @@ -308,70 +394,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.decodeFromCborElement(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.decodeFromCborElement(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.decodeFromCborElement(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.decodeFromCborElement(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.decodeFromCborElement(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.decodeFromCborElement(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.decodeFromCborElement(ValueClassWithUnlabeledByteString.serializer(), struct).x.x) + } @Test diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDefiniteLengthTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDefiniteLengthTest.kt index b19a409308..ab7425d73f 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(), + encodeToCborElement(TypesUmbrella.serializer(), test) + ) + } + ) } } \ No newline at end of file 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..ff0e703032 --- /dev/null +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborDelegatedPropertiesTest.kt @@ -0,0 +1,261 @@ +/* + * 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(), + ) { + @OptIn(DelicateCborApi::class) + content[CborString(key, *keyTags)] = value.also { it.rawTags += 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.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 + } + + else -> key + } + content[mappedKey] = value + } + @OptIn(DelicateCborApi::class) + return MapBackedPerson(content, backing = CborMap(content, *map.rawTags)) + } + } + + 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)] = CborInteger(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 CborInteger(label)] = v + } + @OptIn(DelicateCborApi::class) + cborEncoder.encodeCborElement(CborMap(out, *value.backing.rawTags)) + } + + 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(CborInteger(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) + } +} 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..a969fb5d7f --- /dev/null +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementEqualityTest.kt @@ -0,0 +1,317 @@ +package kotlinx.serialization.cbor + +import kotlin.test.* + +class CborElementEqualityTest { + + //TODO ULONG MIN VALUE TESTS + + @Test + fun testCborPositiveIntEquality() { + 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) + 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 = 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, CborInteger(42u) as CborElement) + assertNotEquals(int1 as CborElement, CborInteger(42u)) + } + + @Test + fun testCborDoubleEquality() { + 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()) + 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", 1u) + + assertEquals(string1, string2) + assertEquals(string1.hashCode(), string2.hashCode()) + assertNotEquals(string1, string3) + assertNotEquals(string1, string4) + assertNotEquals(string1, null as CborElement?) + assertNotEquals(string1 as CborElement, CborInteger(123u)) + assertNotEquals(string1, CborInteger(123u) as CborElement) + } + + @Test + fun testCborBooleanEquality() { + val bool1 = CborBoolean(true) + val bool2 = CborBoolean(true) + val bool3 = CborBoolean(false) + val bool4 = CborBoolean(true, 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, 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(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 testCborArrayEquality() { + 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()) + 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 CborInteger(1u), + CborString("key2") to CborString("value") + ) + ) + val map2 = CborMap( + mapOf( + CborString("key1") to CborInteger(1u), + CborString("key2") to CborString("value") + ) + ) + val map3 = CborMap( + mapOf( + CborString("key1") to CborInteger(2u), + CborString("key2") to CborString("value") + ) + ) + val map4 = CborMap( + mapOf( + CborString("key1") to CborInteger(1u), + CborString("key2") to CborString("value") + ), 1u + ) + val map5 = CborMap( + mapOf( + CborString("key1") to CborInteger(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", tags = tags1) + val string2 = CborString("test", tags = tags2) + val string3 = CborString("test", tags = tags3) + + assertEquals(string1, string2) + assertEquals(string1.hashCode(), string2.hashCode()) + assertNotEquals(string1, string3) + } + + @Test + fun testEmptyCollectionsEquality() { + val emptyList1 = CborArray(emptyList()) + val emptyList2 = CborArray(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 CborArray( + listOf( + CborInteger(1u), + CborMap(mapOf(CborString("inner") to CborNull())) + ) + ) + ) + ) + val nested2 = CborMap( + mapOf( + CborString("list") to CborArray( + listOf( + CborInteger(1u), + CborMap(mapOf(CborString("inner") to CborNull())) + ) + ) + ) + ) + val nested3 = CborMap( + mapOf( + CborString("list") to CborArray( + listOf( + CborInteger(2u), + CborMap(mapOf(CborString("inner") to CborNull())) + ) + ) + ) + ) + + assertEquals(nested1, nested2) + assertEquals(nested1.hashCode(), nested2.hashCode()) + assertNotEquals(nested1, nested3) + } + + @Test + fun testReflexiveEquality() { + val elements = listOf( + CborInteger(42u), + CborInteger(-42), + CborFloat(3.14), + CborString("test"), + CborBoolean(true), + CborByteString(byteArrayOf(1, 2, 3)), + CborNull(), + CborArray(listOf(CborInteger(1u))), + CborMap(mapOf(CborString("key") to CborInteger(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( + 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(CborInteger(1u))) to CborArray(listOf(CborInteger(1u))), + CborMap(mapOf(CborString("key") to CborInteger(1u))) to CborMap( + mapOf( + CborString("key") to CborInteger( + 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") + } +} 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..5094c5d992 --- /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, CborInteger(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 typedCborArrayDeserializationFailsOnNonListInput() { + val cbor = Cbor {} + val bytes = cbor.encodeToByteArray(CborElement.serializer(), CborInteger(1)) + assertFailsWith { + cbor.decodeFromByteArray(CborArray.serializer(), bytes) + } + assertFailsWith { + cbor.decodeFromCborElement(CborArray.serializer(), CborInteger(1)) + } + } + + @Test + fun typedCborIntDeserializationFailsOnNonIntInput() { + val cbor = Cbor {} + val bytes = cbor.encodeToByteArray(CborElement.serializer(), CborArray(listOf(CborInteger(1)))) + assertFailsWith { + cbor.decodeFromByteArray(CborInteger.serializer(), bytes) + } + assertFailsWith { + cbor.decodeFromCborElement(CborInteger.serializer(), CborArray(listOf(CborInteger(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))) + } +} 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..c6deca55a0 --- /dev/null +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborElementTest.kt @@ -0,0 +1,1237 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package kotlinx.serialization.cbor + +import kotlinx.serialization.* +import kotlinx.serialization.cbor.internal.* +import kotlin.test.* + +class CborElementTest { + + private val cbor = Cbor {} + + @Test + fun testEncodeToCborElementRootPrimitiveInt() { + val element = cbor.encodeToCborElement(42) + assertEquals(CborInteger(42), element) + assertEquals(42, cbor.decodeFromCborElement(element)) + } + + @Test + fun testEncodeToCborElementRootPrimitiveByteArrayAlwaysUseByteString() { + val configured = Cbor { alwaysUseByteString = true } + val element = configured.encodeToCborElement(byteArrayOf(1, 2, 3)) + assertTrue(element is CborByteString) + assertTrue(element.toByteArray().contentEquals(byteArrayOf(1, 2, 3))) + assertTrue(configured.decodeFromCborElement(element).contentEquals(byteArrayOf(1, 2, 3))) + } + + @Serializable + private data class Wrapped(val x: Int) + + @Serializable + private data class Wrapper(val datum: Wrapped?) + + @Test + fun testEncodeDecodeNullableClassViaCborElement() { + val wrapper = Wrapper(null) + val element = cbor.encodeToCborElement(wrapper) + assertEquals(wrapper, cbor.decodeFromCborElement(element)) + } + + @Test + fun testEncodeDecodeRootListViaCborElement() { + val value = listOf(1, 2, 3) + val element = cbor.encodeToCborElement(value) + assertTrue(element is CborArray) + assertEquals(value, cbor.decodeFromCborElement>(element)) + } + + @Test + fun testCborNull() { + val nullElement = CborNull() + val nullBytes = cbor.encodeToByteArray(nullElement) + val decodedNull = cbor.decodeFromByteArray(nullBytes) + assertEquals(nullElement, decodedNull) + } + + @Test + fun testCborNumber() { + val numberElement = CborInteger(42u) + val numberBytes = cbor.encodeToByteArray(numberElement) + val decodedNumber = cbor.decodeFromByteArray(numberBytes) + assertEquals(numberElement, decodedNumber) + assertEquals(42uL, (decodedNumber as CborInteger).absoluteValue) + } + + @Test + fun testCborNumberZero() { + val numberElement = CborInteger(0uL) + assertEquals(numberElement, CborInteger(0)) + assertEquals(numberElement.isPositive, true) + assertEquals(numberElement.absoluteValue, 0uL) + val numberBytes = cbor.encodeToByteArray(numberElement) + val decodedNumber = cbor.decodeFromByteArray(numberBytes) + assertEquals(numberElement, decodedNumber) + assertEquals(0uL, (decodedNumber as CborInteger).absoluteValue) + } + + @Test + fun testCborNumberMax() { + val numberElement = CborInteger(ULong.MAX_VALUE) + assertEquals(numberElement.isPositive, true) + 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).absoluteValue) + } + + @Test + fun testCborNumberMaxHalv() { + val numberElement = CborInteger(Long.MAX_VALUE) + assertEquals(numberElement.isPositive, true) + 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).absoluteValue) + } + + + @Test + fun testCborNumberMin() { + val numberElement = CborInteger(ULong.MAX_VALUE, isPositive = false) + assertEquals(numberElement.isPositive, false) + 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).absoluteValue) + + assertNull(numberElement.longOrNull) + assertFailsWith { numberElement.long } + assertFailsWith { cbor.decodeFromCborElement(numberElement) } + } + + + @Test + fun testCborNumberMinHalv() { + val numberElement = CborInteger(Long.MAX_VALUE.toULong(), isPositive = false) + assertEquals(numberElement.isPositive, false) + 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).absoluteValue) + + val long = cbor.decodeFromCborElement(numberElement) + + assertEquals(Long.MIN_VALUE+1, long) + assertEquals(Long.MIN_VALUE + 1, numberElement.long) + } + + + + @Test + fun testCborNumberLong() { + assertEquals(Long.MAX_VALUE, CborInteger(Long.MAX_VALUE).long) + assertEquals(Long.MIN_VALUE, CborInteger(Long.MIN_VALUE).long) + } + + @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).value) + } + + @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).value) + } + + @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).toByteArray().contentEquals(byteArray)) + } + + @Test + fun testCborArray() { + val listElement = CborArray( + listOf( + CborInteger(1u), + CborString("two"), + CborBoolean(true), + CborNull() + ) + ) + val listBytes = cbor.encodeToByteArray(listElement) + val decodedList = cbor.decodeFromByteArray(listBytes) + + // Verify the type and size + assertTrue(decodedList is CborArray) + assertEquals(4, decodedList.size) + + // Verify individual elements + assertTrue(decodedList[0] is CborInteger) + assertEquals(1uL, (decodedList[0] as CborInteger).absoluteValue) + + assertTrue(decodedList[1] is CborString) + assertEquals("two", (decodedList[1] as CborString).value) + + assertTrue(decodedList[2] is CborBoolean) + assertEquals(true, (decodedList[2] as CborBoolean).value) + + assertTrue(decodedList[3] is CborNull) + } + + @Test + fun testCborMap() { + val mapElement = CborMap( + mapOf( + CborString("key1") to CborInteger(42u), + CborString("key2") to CborString("value"), + CborInteger(3u) to CborBoolean(true), + CborNull() to CborNull() + ) + ) + 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 + assertTrue(decodedMap is CborMap) + assertEquals(4, decodedMap.size) + + // Verify individual entries + assertTrue(decodedMap.containsKey(CborString("key1"))) + val value1 = decodedMap[CborString("key1")] + assertTrue(value1 is CborInteger) + assertEquals(42uL, (value1 as CborInteger).absoluteValue) + + assertTrue(decodedMap.containsKey(CborString("key2"))) + val value2 = decodedMap[CborString("key2")] + assertTrue(value2 is CborString) + assertEquals("value", (value2 as CborString).value) + + assertTrue(decodedMap.containsKey(CborInteger(3u))) + val value3 = decodedMap[CborInteger(3u)] + assertTrue(value3 is CborBoolean) + assertEquals(true, (value3 as CborBoolean).value) + + assertTrue(decodedMap.containsKey(CborNull())) + val value4 = decodedMap[CborNull()] + assertTrue(value4 is CborNull) + } + + @Test + fun testComplexNestedStructure() { + // Create a complex nested structure with maps and lists + val complexElement = CborMap( + mapOf( + CborString("primitives") to CborArray( + listOf( + CborInteger(123u), + CborString("text"), + CborBoolean(false), + CborByteString(byteArrayOf(10, 20, 30)), + CborNull() + ) + ), + CborString("nested") to CborMap( + mapOf( + CborString("inner") to CborArray( + listOf( + CborInteger(1u), + CborInteger(2u) + ) + ), + CborString("empty") to CborArray(emptyList()) + ) + ) + ) + ) + + val complexBytes = cbor.encodeToByteArray(complexElement) + val decodedComplex = cbor.decodeFromByteArray(complexBytes) + + // Verify the type + assertTrue(decodedComplex is CborMap) + + // Verify the primitives list + assertTrue(decodedComplex.containsKey(CborString("primitives"))) + val primitivesValue = decodedComplex[CborString("primitives")] + assertTrue(primitivesValue is CborArray) + + assertEquals(5, primitivesValue.size) + + assertTrue(primitivesValue[0] is CborInteger) + assertEquals(123uL, (primitivesValue[0] as CborInteger).absoluteValue) + + assertTrue(primitivesValue[1] is CborString) + assertEquals("text", (primitivesValue[1] as CborString).value) + + assertTrue(primitivesValue[2] is CborBoolean) + assertEquals(false, (primitivesValue[2] as CborBoolean).value) + + assertTrue(primitivesValue[3] is CborByteString) + assertTrue((primitivesValue[3] as CborByteString).toByteArray().contentEquals(byteArrayOf(10, 20, 30))) + + assertTrue(primitivesValue[4] is CborNull) + + // Verify the nested map + assertTrue(decodedComplex.containsKey(CborString("nested"))) + val nestedValue = decodedComplex[CborString("nested")] + assertTrue(nestedValue is CborMap) + + assertEquals(2, nestedValue.size) + + // Verify the inner list + assertTrue(nestedValue.containsKey(CborString("inner"))) + val innerValue = nestedValue[CborString("inner")] + assertTrue(innerValue is CborArray) + + assertEquals(2, innerValue.size) + + assertTrue(innerValue[0] is CborInteger) + assertEquals(1uL, (innerValue[0] as CborInteger).absoluteValue) + + assertTrue(innerValue[1] is CborInteger) + assertEquals(2uL, (innerValue[1] as CborInteger).absoluteValue) + + // Verify the empty list + assertTrue(nestedValue.containsKey(CborString("empty"))) + val emptyValue = nestedValue[CborString("empty")] + assertTrue(emptyValue is CborArray) + val empty = emptyValue + + assertEquals(0, empty.size) + } + + @Test + fun testDecodePositiveInt() { + // Test data from CborParserTest.testParseIntegers + val element = cbor.decodeFromHexString("0C") as CborInteger + assertEquals(12uL, element.absoluteValue) + } + + @Test + fun testDecodeStrings() { + // Test data from CborParserTest.testParseStrings + val element = cbor.decodeFromHexString("6568656C6C6F") + assertTrue(element is CborString) + assertEquals("hello", element.value) + + val longStringElement = + cbor.decodeFromHexString("7828737472696E672074686174206973206C6F6E676572207468616E2032332063686172616374657273") + assertTrue(longStringElement is CborString) + assertEquals("string that is longer than 23 characters", longStringElement.value) + } + + @Test + fun testDecodeFloatingPoint() { + // Test data from CborParserTest.testParseDoubles + val doubleElement = cbor.decodeFromHexString("fb7e37e43c8800759c") + assertTrue(doubleElement is CborFloat) + assertEquals(1e+300, doubleElement.value) + + val floatElement = cbor.decodeFromHexString("fa47c35000") + assertTrue(floatElement is CborFloat) + assertEquals(100000.0f, floatElement.value.toFloat()) + } + + @Test + fun testDecodeByteString() { + // Test data from CborParserTest.testRfc7049IndefiniteByteStringExample + val element = cbor.decodeFromHexString("5F44aabbccdd43eeff99FF") + assertTrue(element is CborByteString) + val byteString = element as CborByteString + val expectedBytes = HexConverter.parseHexBinary("aabbccddeeff99") + assertTrue(byteString.toByteArray().contentEquals(expectedBytes)) + } + + @Test + fun testDecodeArray() { + // Test data from CborParserTest.testSkipCollections + val element = cbor.decodeFromHexString("830118ff1a00010000") + assertTrue(element is CborArray) + val list = element as CborArray + assertEquals(3, list.size) + assertEquals(1uL, (list[0] as CborInteger).absoluteValue) + assertEquals(255uL, (list[1] as CborInteger).absoluteValue) + assertEquals(65536uL, (list[2] as CborInteger).absoluteValue) + } + + @Test + fun testDecodeMap() { + // Test data from CborParserTest.testSkipCollections + val element = cbor.decodeFromHexString("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 = + cbor.decodeFromHexString("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.toByteArray().contentEquals(expectedBytes)) + + // Check the text string + assertEquals(CborString("Hello world"), map[CborString("b")]) + + // Check the array + val array = map[CborString("c")] as CborArray + 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(CborInteger(1u), nestedMap[CborString("1")]) + assertEquals(CborInteger(2u), nestedMap[CborString("2")]) + assertEquals(CborInteger(3u), nestedMap[CborString("3")]) + } + + @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)) + + // 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) + 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 = CborInteger(1u), + cborInt = CborInteger(-1), + tagged = 26 + ), + "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472406b63626f72456c656d656e74f46f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564d90921181aff" + ) + .let { (cbor, obj, hex) -> + 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 + }, + MixedBag( + str = "A string, is a string, is a string", + bStr = null, + cborElement = CborBoolean(false), + cborPositiveInt = CborInteger(1u), + cborInt = CborInteger(-1), + tagged = 26 + ), + "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472f66b63626f72456c656d656e74f46f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564d90921181aff" + ) + .let { (cbor, obj, hex) -> + 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 + }, + MixedBag( + str = "A string, is a string, is a string", + bStr = null, + cborElement = CborMap(mapOf(CborByteString(byteArrayOf(1, 3, 3, 7)) to CborNull())), + cborPositiveInt = CborInteger(1u), + cborInt = CborInteger(-1), + tagged = 26 + ), + "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472f66b63626f72456c656d656e74bf4401030307f6ff6f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564d90921181aff" + ) + .let { (cbor, obj, hex) -> + 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 + }, + MixedBag( + str = "A string, is a string, is a string", + bStr = null, + cborElement = CborNull(), + cborPositiveInt = CborInteger(1u), + cborInt = CborInteger(-1), + tagged = 26 + ), + "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472f66b63626f72456c656d656e74f66f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564d90921181aff" + ) + .let { (cbor, obj, hex) -> + val struct = cbor.encodeToCborElement(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 = CborInteger(1u), + cborInt = CborInteger(-1), + tagged = 26 + ), + "bf6373747278224120737472696e672c206973206120737472696e672c206973206120737472696e676462537472c1c3406b63626f72456c656d656e74f46f63626f72506f736974697665496e74016763626f72496e7420d82a66746167676564d90921181aff" + ) + .let { (cbor, obj, hex) -> + 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 = 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 = CborInteger(1u), + cborInt = CborInteger(-1), + tagged = 26 + ), + "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)) + assertNotEquals(obj, cbor.decodeFromCborElement(struct)) + assertNotEquals(obj, cbor.decodeFromHexString(hex)) + assertEquals(expectedDecoded, cbor.decodeFromCborElement(struct)) + assertEquals(expectedDecoded, cbor.decodeFromHexString(hex)) + } + + } + + @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 + ) + + 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 + ) + } + + @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 + ) + + 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 + ) + } + + @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 + ) + } + + @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 + ) + } + + @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 + ) + } + + @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 + ) + } + + @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)) + } + + @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()) + assertEquals(element, cbor.decodeFromByteArray(bytes)) + } + + @OptIn(ExperimentalSerializationApi::class, ExperimentalUnsignedTypes::class) + @Test + fun testEncodeCborElementWritesTaggedElements() { + val cbor = Cbor { + encodeKeyTags = true + encodeValueTags = true + } + 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)) + } + + @Test + fun testRootCborElementTagsRespectEncodeValueTags() { + val element = CborBoolean(false, 1u) + + assertEquals("f4", cbor.encodeToHexString(CborElement.serializer(), element)) + assertEquals("c1f4", Cbor { encodeValueTags = true }.encodeToHexString(CborElement.serializer(), element)) + } + + @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) + ) + } + + @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) + ) + } + + @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) + ) + } + + @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) + ) + } + + @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) + ) + } + + @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)) + ) + } + } + + @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)) + ) + } + + @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) + ) + ) + } + + @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) + ) + ) + } + + @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) + ) + ) + } + + @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 + ) + } + + @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 + ) + } + + @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")))) + ) + } + + @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 + ) + } + + @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") + ) + } + + @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))) + + val taggedList = CborArray(listOf(CborInteger(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 +data class MixedBag( + val str: String, + val bStr: CborByteString?, + val cborElement: CborElement?, + val cborPositiveInt: CborPrimitive, + val cborInt: CborInteger, + @KeyTags(42u) + @ValueTags(2337u) + val tagged: Int +) + + +@Serializable +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, +) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborIsoTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborIsoTest.kt index 49ef3390ae..639d69a22a 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborIsoTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborIsoTest.kt @@ -27,6 +27,11 @@ class CborIsoTest { } assertEquals(reference, cbor.decodeFromHexString(DataClass.serializer(), referenceHexString)) 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)) } @Serializable diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborLabelTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborLabelTest.kt index 0ecb5283db..06b82a9d63 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.encodeToCborElement(ClassWithCborLabel.serializer(), reference) + assertEquals(reference, cbor.decodeFromCborElement(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.encodeToCborElement(ClassWithCborLabel.serializer(), reference) + assertEquals(reference, cbor.decodeFromCborElement(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.encodeToCborElement(ClassWithCborLabelAndTag.serializer(), referenceWithTag) + assertEquals(referenceWithTag, cbor.decodeFromCborElement(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.decodeFromCborElement(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.encodeToCborElement(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.decodeFromCborElement(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.encodeToCborElement(ClassWithoutCborLabel.serializer(), referenceWithoutLabel) + assertEquals(referenceWithoutLabel, cbor.decodeFromCborElement(ClassWithoutCborLabel.serializer(), struct)) + assertEquals(referenceHexStringWithoutLabel, cbor.encodeToHexString(CborElement.serializer(), struct)) + } @Serializable 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.") } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborPolymorphismTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborPolymorphismTest.kt index 156f471480..e3a72e27ce 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.encodeToCborElement(A.serializer(), original) + assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hexResultToCheck)) + assertEquals(hexResultToCheck, cbor.encodeToHexString(struct)) + assertEquals(original, cbor.decodeFromCborElement(A.serializer(), struct)) } @Test @@ -35,6 +43,9 @@ class CborPolymorphismTest { ) ) assertSerializedToBinaryAndRestored(obj, SealedBox.serializer(), cbor) + + val struct = cbor.encodeToCborElement(SealedBox.serializer(), obj) + assertEquals(obj, cbor.decodeFromCborElement(SealedBox.serializer(), struct)) } @Test @@ -46,5 +57,9 @@ class CborPolymorphismTest { ) ) assertSerializedToBinaryAndRestored(obj, PolyBox.serializer(), cbor) + + + 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 580f6e462a..68263fc304 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.encodeToCborElement(DataWithTags.serializer(), reference) + assertEquals(struct, structFromHex) + assertEquals(reference, cbor.decodeFromCborElement(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.encodeToCborElement(DataWithTags.serializer(), reference) + assertEquals(structDef, structDefFromHex) + 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.encodeToCborElement(DataWithTags.serializer(), reference) + assertEquals(structCose, structCoseFromHex) + assertEquals(reference, Cbor.CoseCompliant.decodeFromCborElement(DataWithTags.serializer(), structCose)) + assertEquals( reference, Cbor.CoseCompliant.decodeFromHexString(DataWithTags.serializer(), referenceHexStringDefLen) ) + val structCoseFromHexDef = + Cbor.CoseCompliant.decodeFromHexString(CborElement.serializer(), referenceHexStringDefLen) + val structCoseDef = Cbor.CoseCompliant.encodeToCborElement(DataWithTags.serializer(), reference) + assertEquals(structCoseDef, structCoseFromHexDef) + assertEquals(reference, Cbor.CoseCompliant.decodeFromCborElement(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.encodeToCborElement(DataWithTags.serializer(), reference) + assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + assertEquals(reference, cbor.decodeFromCborElement(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.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.decodeFromCborElement(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.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.decodeFromCborElement(DataWithTags.serializer(), struct)) + } + + assertEquals(reference, cborEncodingKeyTags.decodeFromHexString(noKeyTagsDefLen)) + (cborEncodingKeyTags to noKeyTagsDefLen).let { (cbor, hex) -> + 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.decodeFromCborElement(DataWithTags.serializer(), struct)) + } + + + assertEquals(reference, cborEncodingKeyTags.decodeFromHexString(referenceHexString)) + (cborNoKeyTags to noKeyTags).let { (cbor, hex) -> + val struct = cbor.encodeToCborElement(DataWithTags.serializer(), reference) + assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + assertEquals(reference, cbor.decodeFromCborElement(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.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.decodeFromCborElement(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.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.decodeFromCborElement(DataWithTags.serializer(), struct)) + } + + + assertEquals(reference, cborEncodingValueTags.decodeFromHexString(referenceHexString)) + (cborEncodingValueTags to referenceHexString).let { (cbor, hex) -> + val struct = cbor.encodeToCborElement(DataWithTags.serializer(), reference) + assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + assertEquals(reference, cbor.decodeFromCborElement(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.encodeToCborElement(DataWithTags.serializer(), reference) + assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + //struct is missing the tags + assertFailsWith(CborDecodingException::class) { + assertEquals(reference, cbor.decodeFromCborElement(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.encodeToCborElement(DataWithTags.serializer(), reference) + assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + //struct is missing the tags + assertFailsWith(CborDecodingException::class) { + assertEquals(reference, cbor.decodeFromCborElement(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.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.decodeFromCborElement(DataWithTags.serializer(), struct)) + } + + assertEquals(reference, cborEncodingAllVerifyingObjectTags.decodeFromHexString(noTagsDefLen)) + (cborEncodingAllVerifyingObjectTags to noTagsDefLen).let { (cbor, hex) -> + 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.decodeFromCborElement(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.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.decodeFromCborElement(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.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.decodeFromCborElement(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.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.decodeFromCborElement(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.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.decodeFromCborElement(DataWithTags.serializer(), struct)) + } + + assertEquals(reference, cborEncodingAllVerifyingObjectTags.decodeFromHexString(referenceHexString)) + (cborEncodingAllVerifyingObjectTags to referenceHexString).let { (cbor, hex) -> + val struct = cbor.encodeToCborElement(DataWithTags.serializer(), reference) + assertEquals(struct, cbor.decodeFromHexString(CborElement.serializer(), hex)) + assertEquals(reference, cbor.decodeFromCborElement(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.decodeFromCborElement( + 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.decodeFromCborElement(cbor.decodeFromHexString(wrongTag55ForPropertyC))) } } @@ -571,27 +655,65 @@ class CborTaggedTest { assertEquals(referenceHexString, cbor.encodeToHexString(ClassAsTagged.serializer(), reference)) assertEquals(reference, cbor.decodeFromHexString(ClassAsTagged.serializer(), referenceHexString)) + (cbor to referenceHexString).let { (cbor, hexString) -> + val struct = cbor.encodeToCborElement(reference) + assertEquals(hexString, cbor.encodeToHexString(CborElement.serializer(), struct)) + assertEquals(cbor.decodeFromHexString(hexString), struct) + assertEquals(reference, cbor.decodeFromCborElement(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.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.decodeFromCborElement(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.encodeToCborElement(reference) + assertEquals(hexString, cbor.encodeToHexString(CborElement.serializer(), struct)) + assertEquals(cbor.decodeFromHexString(hexString), struct) + assertEquals(reference, cbor.decodeFromCborElement(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.encodeToCborElement(reference) + assertEquals(hexString, cbor.encodeToHexString(CborElement.serializer(), struct)) + assertEquals(cbor.decodeFromHexString(hexString), struct) + assertEquals(reference, cbor.decodeFromCborElement(ClassAsTagged.serializer(), struct)) + } assertContains( assertFailsWith(CborDecodingException::class) { cbor.decodeFromHexString(ClassAsTagged.serializer(), untaggedHexString) }.message ?: "", "do not match expected tags" ) + assertContains( + assertFailsWith(CborDecodingException::class) { + cbor.decodeFromCborElement( + ClassAsTagged.serializer(), + cbor.decodeFromHexString(CborElement.serializer(), untaggedHexString) + ) + }.message ?: "", "do not match expected tags" + ) /** * 81 # array(1) @@ -603,6 +725,10 @@ class CborTaggedTest { */ val listOfObjectTagged = listOf(reference) assertEquals("81d90539a163616c6713", Cbor.CoseCompliant.encodeToHexString(listOfObjectTagged)) + assertEquals( + "81d90539a163616c6713", + Cbor.CoseCompliant.encodeToHexString(Cbor.CoseCompliant.encodeToCborElement(listOfObjectTagged)) + ) } @@ -674,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.encodeToCborElement(reference) + assertEquals(struct, cbor.decodeFromHexString(hex)) + assertEquals(reference, cbor.decodeFromCborElement(struct)) + } + assertEquals( "More tags found than the 1 tags specified", @@ -681,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.decodeFromCborElement( + NestedTagged.serializer(), + cbor.decodeFromHexString(CborElement.serializer(), referenceHexStringWithBogusTag) + ) + }.message + ) assertEquals( "CBOR tags null do not match expected tags [19]", @@ -688,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.decodeFromCborElement( + 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.encodeToCborElement(reference) + assertEquals(struct, cbor.decodeFromHexString(hex)) + assertEquals(reference, cbor.decodeFromCborElement(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.encodeToCborElement(reference) + //there are more tags in the string + assertNotEquals(struct, cbor.decodeFromHexString(hex)) + assertEquals(reference, cbor.decodeFromCborElement(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.encodeToCborElement(reference) + assertEquals(struct, cbor.decodeFromHexString(hex)) + assertEquals(reference, cbor.decodeFromCborElement(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.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.decodeFromCborElement(struct)) + } assertContains( assertFailsWith(CborDecodingException::class) { @@ -747,21 +910,21 @@ class CborTaggedTest { assertContains( assertFailsWith(CborDecodingException::class) { - Cbor { - encodeKeyTags = true - encodeValueTags = true - encodeObjectTags = true - verifyKeyTags = true - verifyValueTags = true - verifyObjectTags = false - }.decodeFromHexString( + cbor.decodeFromCborElement( + 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 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..8fed710fb3 --- /dev/null +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborUnsignedInteroperabilityTest.kt @@ -0,0 +1,47 @@ +@file:OptIn(ExperimentalUnsignedTypes::class, ExperimentalSerializationApi::class) + +package kotlinx.serialization.cbor + +import kotlinx.serialization.* +import kotlin.test.* + +class CborUnsignedInteroperabilityTest { + + @Test + fun testUnsignedInlineTypesUseCborUnsignedIntegerEncoding() { + // 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.absoluteValue) + + 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)) + + val canonicalULongMax = "1bffffffffffffffff" + assertEquals(ULong.MAX_VALUE, Cbor.decodeFromHexString(canonicalULongMax)) + assertEquals(canonicalULongMax, Cbor.encodeToHexString(ULong.MAX_VALUE)) + + // Structured decoding from CborElement also supports unsigned values beyond Long range. + assertEquals(ULong.MAX_VALUE, Cbor.decodeFromCborElement(CborInteger(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(CborInteger(-1)) } + } +} diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt index 330d4ff0f0..1f9718ea5a 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt @@ -27,10 +27,14 @@ class CbrWriterTest { HexConverter.parseHexBinary("cafe"), HexConverter.parseHexBinary("cafe") ) + val encoded = + "bf637374726d48656c6c6f2c20776f726c64216169182a686e756c6c61626c65f6646c6973749f61616162ff636d6170bf01f502f4ff65696e6e6572bf6161636c6f6cff6a696e6e6572734c6973749fbf6161636b656bffff6a62797465537472696e6742cafe696279746541727261799f383521ffff" assertEquals( - "bf637374726d48656c6c6f2c20776f726c64216169182a686e756c6c61626c65f6646c6973749f61616162ff636d6170bf01f502f4ff65696e6e6572bf6161636c6f6cff6a696e6e6572734c6973749fbf6161636b656bffff6a62797465537472696e6742cafe696279746541727261799f383521ffff", + encoded, Cbor.encodeToHexString(TypesUmbrella.serializer(), test) ) + val struct = Cbor.encodeToCborElement(test) + assertEquals(Cbor.decodeFromByteArray(encoded.hexToByteArray()), struct) } @Test @@ -46,10 +50,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.encodeToCborElement(test) + assertEquals(Cbor.decodeFromByteArray(encoded.hexToByteArray()), struct) } @Test @@ -62,10 +70,14 @@ class CbrWriterTest { true, 'a' ) + val encoded = + "bf63696e741a00018894646c6f6e671b7fffffffffffffff65666c6f6174fa4228000066646f75626c65fb4271fb0c5a2b700067626f6f6c65616ef564636861721861ff" assertEquals( - "bf63696e741a00018894646c6f6e671b7fffffffffffffff65666c6f6174fa4228000066646f75626c65fb4271fb0c5a2b700067626f6f6c65616ef564636861721861ff", + encoded, Cbor.encodeToHexString(NumberTypesUmbrella.serializer(), test) ) + val struct = Cbor.encodeToCborElement(test) + assertEquals(Cbor.decodeFromByteArray(encoded.hexToByteArray()), struct) } @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..311925dad4 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/SampleClasses.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/SampleClasses.kt @@ -73,6 +73,7 @@ data class NullableByteString( @ByteString val byteString: ByteArray? ) { override fun equals(other: Any?): Boolean { + if (this === other) return true if (other == null || this::class != other::class) return false 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..75bb6f63b3 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=[], bytes=h'666f6f)})" + ) + } + @Test fun testExampleFormats04() { captureOutput("ExampleFormats04") { example.exampleFormats04.main() }.verifyOutputLines(