Skip to content

Commit c744df7

Browse files
committed
WIP strucutred cbor
1 parent 297b333 commit c744df7

File tree

8 files changed

+1151
-5
lines changed

8 files changed

+1151
-5
lines changed

formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborDecoder.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,15 @@ public interface CborDecoder : Decoder {
3131
* Exposes the current [Cbor] instance and all its configuration flags. Useful for low-level custom serializers.
3232
*/
3333
public val cbor: Cbor
34+
35+
/**
36+
* Decodes the next element in the current input as [CborElement].
37+
* The type of the decoded element depends on the current state of the input and, when received
38+
* by [serializer][KSerializer] in its [KSerializer.serialize] method, the type of the token directly matches
39+
* the [kind][SerialDescriptor.kind].
40+
*
41+
* This method is allowed to invoke only as the part of the whole deserialization process of the class,
42+
* calling this method after invoking [beginStructure] or any `decode*` method will lead to unspecified behaviour.
43+
*/
44+
public fun decodeCborElement(): CborElement
3445
}
Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
/*
2+
* Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
@file:Suppress("unused")
6+
7+
package kotlinx.serialization.cbor
8+
9+
import kotlinx.serialization.*
10+
import kotlinx.serialization.cbor.internal.*
11+
12+
/**
13+
* Class representing single CBOR element.
14+
* Can be [CborPrimitive], [CborMap] or [CborList].
15+
*
16+
* [CborElement.toString] properly prints CBOR tree as a human-readable representation.
17+
* Whole hierarchy is serializable, but only when used with [Cbor] as [CborElement] is purely CBOR-specific structure
18+
* which has a meaningful schemaless semantics only for CBOR.
19+
*
20+
* The whole hierarchy is [serializable][Serializable] only by [Cbor] format.
21+
*/
22+
@Serializable(with = CborElementSerializer::class)
23+
public sealed class CborElement
24+
25+
/**
26+
* Class representing CBOR primitive value.
27+
* CBOR primitives include numbers, strings, booleans, byte arrays and special null value [CborNull].
28+
*/
29+
@Serializable(with = CborPrimitiveSerializer::class)
30+
public sealed class CborPrimitive : CborElement() {
31+
/**
32+
* Content of given element as string. For [CborNull], this method returns a "null" string.
33+
* [CborPrimitive.contentOrNull] should be used for [CborNull] to get a `null`.
34+
*/
35+
public abstract val content: String
36+
37+
public override fun toString(): String = content
38+
}
39+
40+
/**
41+
* Sealed class representing CBOR number value.
42+
* Can be either [Signed] or [Unsigned].
43+
*/
44+
@Serializable(with = CborNumberSerializer::class)
45+
public sealed class CborNumber : CborPrimitive() {
46+
/**
47+
* Returns the value as a [Byte].
48+
*/
49+
public abstract val byte: Byte
50+
51+
/**
52+
* Returns the value as a [Short].
53+
*/
54+
public abstract val short: Short
55+
56+
/**
57+
* Returns the value as an [Int].
58+
*/
59+
public abstract val int: Int
60+
61+
/**
62+
* Returns the value as a [Long].
63+
*/
64+
public abstract val long: Long
65+
66+
/**
67+
* Returns the value as a [Float].
68+
*/
69+
public abstract val float: Float
70+
71+
/**
72+
* Returns the value as a [Double].
73+
*/
74+
public abstract val double: Double
75+
76+
/**
77+
* Class representing a signed CBOR number value.
78+
*/
79+
public class Signed(@Contextual private val value: Number) : CborNumber() {
80+
override val content: String get() = value.toString()
81+
override val byte: Byte get() = value.toByte()
82+
override val short: Short get() = value.toShort()
83+
override val int: Int get() = value.toInt()
84+
override val long: Long get() = value.toLong()
85+
override val float: Float get() = value.toFloat()
86+
override val double: Double get() = value.toDouble()
87+
88+
override fun equals(other: Any?): Boolean {
89+
if (this === other) return true
90+
if (other == null) return false
91+
92+
when (other) {
93+
is Signed -> {
94+
// Compare as double to handle different numeric types
95+
return when {
96+
// For integers, compare as long to avoid precision loss
97+
value is Byte || value is Short || value is Int || value is Long ||
98+
other.value is Byte || other.value is Short || other.value is Int || other.value is Long -> {
99+
value.toLong() == other.value.toLong()
100+
}
101+
// For floating point, compare as double
102+
else -> {
103+
value.toDouble() == other.value.toDouble()
104+
}
105+
}
106+
}
107+
is Unsigned -> {
108+
// Only compare if both are non-negative integers
109+
if (value is Byte || value is Short || value is Int || value is Long) {
110+
val longValue = value.toLong()
111+
return longValue >= 0 && longValue.toULong() == other.long.toULong()
112+
}
113+
return false
114+
}
115+
else -> return false
116+
}
117+
}
118+
119+
override fun hashCode(): Int = value.hashCode()
120+
}
121+
122+
/**
123+
* Class representing an unsigned CBOR number value.
124+
*/
125+
public class Unsigned(private val value: ULong) : CborNumber() {
126+
override val content: String get() = value.toString()
127+
override val byte: Byte get() = value.toByte()
128+
override val short: Short get() = value.toShort()
129+
override val int: Int get() = value.toInt()
130+
override val long: Long get() = value.toLong()
131+
override val float: Float get() = value.toFloat()
132+
override val double: Double get() = value.toDouble()
133+
134+
override fun equals(other: Any?): Boolean {
135+
if (this === other) return true
136+
if (other == null) return false
137+
138+
when (other) {
139+
is Unsigned -> {
140+
return value == other.long.toULong()
141+
}
142+
is Signed -> {
143+
// Only compare if the signed value is non-negative
144+
val otherLong = other.long
145+
return otherLong >= 0 && value == otherLong.toULong()
146+
}
147+
else -> return false
148+
}
149+
}
150+
151+
override fun hashCode(): Int = value.hashCode()
152+
}
153+
}
154+
155+
/**
156+
* Class representing CBOR string value.
157+
*/
158+
@Serializable(with = CborStringSerializer::class)
159+
public class CborString(private val value: String) : CborPrimitive() {
160+
override val content: String get() = value
161+
162+
override fun equals(other: Any?): Boolean {
163+
if (this === other) return true
164+
if (other == null || this::class != other::class) return false
165+
other as CborString
166+
return value == other.value
167+
}
168+
169+
override fun hashCode(): Int = value.hashCode()
170+
}
171+
172+
/**
173+
* Class representing CBOR boolean value.
174+
*/
175+
@Serializable(with = CborBooleanSerializer::class)
176+
public class CborBoolean(private val value: Boolean) : CborPrimitive() {
177+
override val content: String get() = value.toString()
178+
179+
/**
180+
* Returns the boolean value.
181+
*/
182+
public val boolean: Boolean get() = value
183+
184+
override fun equals(other: Any?): Boolean {
185+
if (this === other) return true
186+
if (other == null || this::class != other::class) return false
187+
other as CborBoolean
188+
return value == other.value
189+
}
190+
191+
override fun hashCode(): Int = value.hashCode()
192+
}
193+
194+
/**
195+
* Class representing CBOR byte string value.
196+
*/
197+
@Serializable(with = CborByteStringSerializer::class)
198+
public class CborByteString(private val value: ByteArray) : CborPrimitive() {
199+
override val content: String get() = value.contentToString()
200+
201+
/**
202+
* Returns the byte array value.
203+
*/
204+
public val bytes: ByteArray get() = value.copyOf()
205+
206+
override fun equals(other: Any?): Boolean {
207+
if (this === other) return true
208+
if (other == null || this::class != other::class) return false
209+
other as CborByteString
210+
return value.contentEquals(other.value)
211+
}
212+
213+
override fun hashCode(): Int = value.contentHashCode()
214+
}
215+
216+
/**
217+
* Class representing CBOR `null` value
218+
*/
219+
@Serializable(with = CborNullSerializer::class)
220+
public object CborNull : CborPrimitive() {
221+
override val content: String = "null"
222+
}
223+
224+
/**
225+
* Class representing CBOR map, consisting of key-value pairs, where both key and value are arbitrary [CborElement]
226+
*
227+
* Since this class also implements [Map] interface, you can use
228+
* traditional methods like [Map.get] or [Map.getValue] to obtain CBOR elements.
229+
*/
230+
@Serializable(with = CborMapSerializer::class)
231+
public class CborMap(
232+
private val content: Map<CborElement, CborElement>
233+
) : CborElement(), Map<CborElement, CborElement> by content {
234+
public override fun equals(other: Any?): Boolean {
235+
if (this === other) return true
236+
if (other == null || this::class != other::class) return false
237+
other as CborMap
238+
return content == other.content
239+
}
240+
public override fun hashCode(): Int = content.hashCode()
241+
public override fun toString(): String {
242+
return content.entries.joinToString(
243+
separator = ", ",
244+
prefix = "{",
245+
postfix = "}",
246+
transform = { (k, v) -> "$k: $v" }
247+
)
248+
}
249+
}
250+
251+
/**
252+
* Class representing CBOR array, consisting of indexed values, where value is arbitrary [CborElement]
253+
*
254+
* Since this class also implements [List] interface, you can use
255+
* traditional methods like [List.get] or [List.getOrNull] to obtain CBOR elements.
256+
*/
257+
@Serializable(with = CborListSerializer::class)
258+
public class CborList(private val content: List<CborElement>) : CborElement(), List<CborElement> by content {
259+
public override fun equals(other: Any?): Boolean {
260+
if (this === other) return true
261+
if (other == null || this::class != other::class) return false
262+
other as CborList
263+
return content == other.content
264+
}
265+
public override fun hashCode(): Int = content.hashCode()
266+
public override fun toString(): String = content.joinToString(prefix = "[", postfix = "]", separator = ", ")
267+
}
268+
269+
/**
270+
* Convenience method to get current element as [CborPrimitive]
271+
* @throws IllegalArgumentException if current element is not a [CborPrimitive]
272+
*/
273+
public val CborElement.cborPrimitive: CborPrimitive
274+
get() = this as? CborPrimitive ?: error("CborPrimitive")
275+
276+
/**
277+
* Convenience method to get current element as [CborMap]
278+
* @throws IllegalArgumentException if current element is not a [CborMap]
279+
*/
280+
public val CborElement.cborMap: CborMap
281+
get() = this as? CborMap ?: error("CborMap")
282+
283+
/**
284+
* Convenience method to get current element as [CborList]
285+
* @throws IllegalArgumentException if current element is not a [CborList]
286+
*/
287+
public val CborElement.cborList: CborList
288+
get() = this as? CborList ?: error("CborList")
289+
290+
/**
291+
* Convenience method to get current element as [CborNull]
292+
* @throws IllegalArgumentException if current element is not a [CborNull]
293+
*/
294+
public val CborElement.cborNull: CborNull
295+
get() = this as? CborNull ?: error("CborNull")
296+
297+
/**
298+
* Convenience method to get current element as [CborNumber]
299+
* @throws IllegalArgumentException if current element is not a [CborNumber]
300+
*/
301+
public val CborElement.cborNumber: CborNumber
302+
get() = this as? CborNumber ?: error("CborNumber")
303+
304+
/**
305+
* Convenience method to get current element as [CborString]
306+
* @throws IllegalArgumentException if current element is not a [CborString]
307+
*/
308+
public val CborElement.cborString: CborString
309+
get() = this as? CborString ?: error("CborString")
310+
311+
/**
312+
* Convenience method to get current element as [CborBoolean]
313+
* @throws IllegalArgumentException if current element is not a [CborBoolean]
314+
*/
315+
public val CborElement.cborBoolean: CborBoolean
316+
get() = this as? CborBoolean ?: error("CborBoolean")
317+
318+
/**
319+
* Convenience method to get current element as [CborByteString]
320+
* @throws IllegalArgumentException if current element is not a [CborByteString]
321+
*/
322+
public val CborElement.cborByteString: CborByteString
323+
get() = this as? CborByteString ?: error("CborByteString")
324+
325+
/**
326+
* Content of the given element as string or `null` if current element is [CborNull]
327+
*/
328+
public val CborPrimitive.contentOrNull: String? get() = if (this is CborNull) null else content
329+
330+
/**
331+
* Creates a [CborMap] from the given map entries.
332+
*/
333+
public fun CborMap(vararg pairs: Pair<CborElement, CborElement>): CborMap = CborMap(mapOf(*pairs))
334+
335+
/**
336+
* Creates a [CborList] from the given elements.
337+
*/
338+
public fun CborList(vararg elements: CborElement): CborList = CborList(listOf(*elements))
339+
340+
private fun CborElement.error(element: String): Nothing =
341+
throw IllegalArgumentException("Element ${this::class} is not a $element")

formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,9 @@ public interface CborEncoder : Encoder {
3131
* Exposes the current [Cbor] instance and all its configuration flags. Useful for low-level custom serializers.
3232
*/
3333
public val cbor: Cbor
34+
35+
/**
36+
* Encodes the specified [byteArray] as a CBOR byte string.
37+
*/
38+
public fun encodeByteArray(byteArray: ByteArray)
3439
}

0 commit comments

Comments
 (0)