Skip to content

Commit e62191e

Browse files
committed
Support skipping unknown keys when decoding CBOR
Closes #935
1 parent 5c1fdce commit e62191e

File tree

5 files changed

+448
-10
lines changed

5 files changed

+448
-10
lines changed

formats/cbor/api/kotlinx-serialization-cbor.api

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public final class kotlinx/serialization/cbor/ByteString$Impl : kotlinx/serializ
77

88
public abstract class kotlinx/serialization/cbor/Cbor : kotlinx/serialization/BinaryFormat {
99
public static final field Default Lkotlinx/serialization/cbor/Cbor$Default;
10-
public synthetic fun <init> (ZLkotlinx/serialization/modules/SerializersModule;Ljava/lang/Void;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
10+
public synthetic fun <init> (ZZLkotlinx/serialization/modules/SerializersModule;Ljava/lang/Void;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
1111
public fun decodeFromByteArray (Lkotlinx/serialization/DeserializationStrategy;[B)Ljava/lang/Object;
1212
public fun encodeToByteArray (Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)[B
1313
public fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule;
@@ -18,8 +18,10 @@ public final class kotlinx/serialization/cbor/Cbor$Default : kotlinx/serializati
1818

1919
public final class kotlinx/serialization/cbor/CborBuilder {
2020
public final fun getEncodeDefaults ()Z
21+
public final fun getIgnoreUnknownKeys ()Z
2122
public final fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule;
2223
public final fun setEncodeDefaults (Z)V
24+
public final fun setIgnoreUnknownKeys (Z)V
2325
public final fun setSerializersModule (Lkotlinx/serialization/modules/SerializersModule;)V
2426
}
2527

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,19 @@ import kotlinx.serialization.modules.*
2424
* from corresponding Kotlin objects. However, other 3rd-party parsers (e.g. `jackson-dataformat-cbor`) may not accept such maps.
2525
*
2626
* @param encodeDefaults specifies whether default values of Kotlin properties are encoded.
27+
* @param ignoreUnknownKeys specifies if unknown CBOR elements should be ignored (skipped) when decoding.
2728
*/
2829
public sealed class Cbor(
2930
internal val encodeDefaults: Boolean,
31+
internal val ignoreUnknownKeys: Boolean,
3032
override val serializersModule: SerializersModule,
3133
ctorMarker: Nothing? // Marker for the temporary migration
3234
) : BinaryFormat {
3335

3436
/**
3537
* The default instance of [Cbor]
3638
*/
37-
public companion object Default : Cbor(true, EmptySerializersModule, null)
39+
public companion object Default : Cbor(true, false, EmptySerializersModule, null)
3840

3941
override fun <T> encodeToByteArray(serializer: SerializationStrategy<T>, value: T): ByteArray {
4042
val output = ByteArrayOutput()
@@ -50,8 +52,8 @@ public sealed class Cbor(
5052
}
5153
}
5254

53-
private class CborImpl(encodeDefaults: Boolean, serializersModule: SerializersModule) :
54-
Cbor(encodeDefaults, serializersModule, null)
55+
private class CborImpl(encodeDefaults: Boolean, ignoreUnknownKeys: Boolean, serializersModule: SerializersModule) :
56+
Cbor(encodeDefaults, ignoreUnknownKeys, serializersModule, null)
5557

5658
/**
5759
* Creates an instance of [Cbor] configured from the optionally given [Cbor instance][from]
@@ -60,7 +62,7 @@ private class CborImpl(encodeDefaults: Boolean, serializersModule: SerializersMo
6062
public fun Cbor(from: Cbor = Cbor, builderAction: CborBuilder.() -> Unit): Cbor {
6163
val builder = CborBuilder(from)
6264
builder.builderAction()
63-
return CborImpl(builder.encodeDefaults, builder.serializersModule)
65+
return CborImpl(builder.encodeDefaults, builder.ignoreUnknownKeys, builder.serializersModule)
6466
}
6567

6668
/**
@@ -73,6 +75,13 @@ public class CborBuilder internal constructor(cbor: Cbor) {
7375
*/
7476
public var encodeDefaults: Boolean = cbor.encodeDefaults
7577

78+
/**
79+
* Specifies whether encounters of unknown properties in the input CBOR
80+
* should be ignored instead of throwing [SerializationException].
81+
* `false` by default.
82+
*/
83+
public var ignoreUnknownKeys: Boolean = cbor.ignoreUnknownKeys
84+
7685
/**
7786
* Module with contextual and polymorphic serializers to be used in the resulting [Cbor] instance.
7887
*/

formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoding.kt

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -227,10 +227,29 @@ internal open class CborReader(private val cbor: Cbor, protected val decoder: Cb
227227
}
228228

229229
override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
230-
if (!finiteMode && decoder.isEnd() || (finiteMode && readProperties >= size)) return CompositeDecoder.DECODE_DONE
231-
val elemName = decoder.nextString()
232-
readProperties++
233-
val index = descriptor.getElementIndexOrThrow(elemName)
230+
val index = if (cbor.ignoreUnknownKeys) {
231+
val knownIndex: Int
232+
while (true) {
233+
if (isDone()) return CompositeDecoder.DECODE_DONE
234+
val elemName = decoder.nextString()
235+
readProperties++
236+
237+
val index = descriptor.getElementIndex(elemName)
238+
if (index == CompositeDecoder.UNKNOWN_NAME) {
239+
decoder.skipElement()
240+
} else {
241+
knownIndex = index
242+
break
243+
}
244+
}
245+
knownIndex
246+
} else {
247+
if (isDone()) return CompositeDecoder.DECODE_DONE
248+
val elemName = decoder.nextString()
249+
readProperties++
250+
descriptor.getElementIndexOrThrow(elemName)
251+
}
252+
234253
decodeByteArrayAsByteString = descriptor.isByteString(index)
235254
return index
236255
}
@@ -263,6 +282,7 @@ internal open class CborReader(private val cbor: Cbor, protected val decoder: Cb
263282
override fun decodeEnum(enumDescriptor: SerialDescriptor): Int =
264283
enumDescriptor.getElementIndexOrThrow(decoder.nextString())
265284

285+
private fun isDone(): Boolean = !finiteMode && decoder.isEnd() || (finiteMode && readProperties >= size)
266286
}
267287

268288
internal class CborDecoder(private val input: ByteArrayInput) {
@@ -420,6 +440,65 @@ internal class CborDecoder(private val input: ByteArrayInput) {
420440
return result
421441
}
422442

443+
fun skipElement() {
444+
check(!isEnd()) { "Unexpected end marker" }
445+
val lengthStack = mutableListOf<Int>()
446+
447+
do {
448+
var prune = false
449+
450+
if (isEnd()) {
451+
check(lengthStack.lastOrNull() == -1) { "Unexpected end marker" }
452+
lengthStack.removeAt(lengthStack.lastIndex)
453+
prune = true
454+
} else {
455+
val header = curByte and 0b111_00000
456+
val isIndefinite = curByte and 0b000_11111 == 0b000_11111 &&
457+
(header == HEADER_ARRAY || header == HEADER_MAP ||
458+
header == HEADER_BYTE_STRING.toInt() || header == HEADER_STRING.toInt())
459+
460+
if (isIndefinite) {
461+
lengthStack.add(-1)
462+
} else {
463+
val length = when (header) {
464+
HEADER_BYTE_STRING.toInt(), HEADER_STRING.toInt(), HEADER_ARRAY -> readNumber().toInt()
465+
HEADER_MAP -> readNumber().toInt() * 2
466+
else -> when (curByte and 0b000_11111) {
467+
24 -> 1
468+
25 -> 2
469+
26 -> 4
470+
27 -> 8
471+
else -> 0
472+
}
473+
}
474+
475+
if (header == HEADER_ARRAY || header == HEADER_MAP) {
476+
check(length > 0) { "Length must be > 0" }
477+
lengthStack.add(length)
478+
} else {
479+
input.skip(length)
480+
prune = true
481+
}
482+
}
483+
}
484+
485+
if (prune) {
486+
for (i in lengthStack.lastIndex downTo 0) {
487+
when (lengthStack[i]) {
488+
-1 -> break
489+
1 -> lengthStack.removeAt(i)
490+
else -> {
491+
lengthStack[i] = lengthStack[i] - 1
492+
break
493+
}
494+
}
495+
}
496+
}
497+
498+
if (readByte() == -1 && lengthStack.isNotEmpty()) error("Unexpected end of bytes")
499+
} while (lengthStack.isNotEmpty())
500+
}
501+
423502
/**
424503
* Indefinite-length byte sequences contain an unknown number of fixed-length byte sequences (chunks).
425504
*
@@ -438,7 +517,8 @@ internal class CborDecoder(private val input: ByteArrayInput) {
438517
private fun SerialDescriptor.getElementIndexOrThrow(name: String): Int {
439518
val index = getElementIndex(name)
440519
if (index == CompositeDecoder.UNKNOWN_NAME)
441-
throw SerializationException("$serialName does not contain element with name '$name'")
520+
throw SerializationException("$serialName does not contain element with name '$name." +
521+
" You can enable 'CborBuilder.ignoreUnknownKeys' property to ignore unknown keys")
442522
return index
443523
}
444524

formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Streams.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ internal class ByteArrayInput(private var array: ByteArray) {
3333
return copied
3434
}
3535

36+
fun skip(length: Int) {
37+
require(length >= 0) { "Cannot skip $length bytes" }
38+
position += length
39+
}
3640
}
3741

3842
internal class ByteArrayOutput {

0 commit comments

Comments
 (0)