Skip to content

Commit

Permalink
Disallow repeated keys in CBOR (WIP, need to add config switches)
Browse files Browse the repository at this point in the history
Fixes Kotlin#2662 by adding
a `visitKey` method to `CompositeDecoder`; map and set serializers should
call this so that decoders have an opportunity to throw an error when a
duplicate key is detected.

Also fixes a typo in an unrelated method docstring.
  • Loading branch information
timmc committed May 19, 2024
1 parent 194a188 commit a768cf3
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,10 @@ internal constructor(message: String?) : SerializationException(message) {
// This constructor is used by the generated serializers
constructor(index: Int) : this("An unknown field for index $index")
}

/**
* Thrown when a map deserializer encounters a repeated map key (and configuration disallows this.)
*/
@ExperimentalSerializationApi
public class DuplicateMapKeyException(public val key: Any?) :
SerializationException("Duplicate keys not allowed in maps. Key appeared twice: $key")
13 changes: 12 additions & 1 deletion core/commonMain/src/kotlinx/serialization/encoding/Decoding.kt
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ internal inline fun <T : Any> Decoder.decodeIfNullable(deserializer: Deserializa
* [CompositeDecoder] is a part of decoding process that is bound to a particular structured part of
* the serialized form, described by the serial descriptor passed to [Decoder.beginStructure].
*
* Typically, for unordered data, [CompositeDecoder] is used by a serializer withing a [decodeElementIndex]-based
* Typically, for unordered data, [CompositeDecoder] is used by a serializer within a [decodeElementIndex]-based
* loop that decodes all the required data one-by-one in any order and then terminates by calling [endStructure].
* Please refer to [decodeElementIndex] for example of such loop.
*
Expand Down Expand Up @@ -558,6 +558,17 @@ public interface CompositeDecoder {
deserializer: DeserializationStrategy<T?>,
previousValue: T? = null
): T?

/**
* Called after a key has been read.
*
* This could be a map or set key, or anything otherwise intended to be
* distinct within the collection under normal circumstances.
*
* Implementations might use this as a hook for throwing an exception when
* duplicate keys are encountered.
*/
public fun visitKey(key: Any?) { }
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ public sealed class MapLikeSerializer<Key, Value, Collection, Builder : MutableM

final override fun readElement(decoder: CompositeDecoder, index: Int, builder: Builder, checkIndex: Boolean) {
val key: Key = decoder.decodeSerializableElement(descriptor, index, keySerializer)
decoder.visitKey(key)
val vIndex = if (checkIndex) {
decoder.decodeElementIndex(descriptor).also {
require(it == index + 1) { "Value must follow key in a map, index for key: $index, returned index for value: $it" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,17 @@ internal class CborEncoder(private val output: ByteArrayOutput) {
}

private class CborMapReader(cbor: Cbor, decoder: CborDecoder) : CborListReader(cbor, decoder) {
/** Keys that have been seen so far while reading this map. */
private val seenKeys = mutableSetOf<Any?>()

override fun skipBeginToken() = setSize(decoder.startMap() * 2)

override fun visitKey(key: Any?) {
val added = seenKeys.add(key)
if (!added) {
throw DuplicateMapKeyException(key)
}
}
}

private open class CborListReader(cbor: Cbor, decoder: CborDecoder) : CborReader(cbor, decoder) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package kotlinx.serialization.cbor

import kotlinx.serialization.assertFailsWithMessage
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.HexConverter
import kotlinx.serialization.DuplicateMapKeyException
import kotlin.test.Test
import kotlin.test.assertEquals

class CborStrictModeTest {

/** Duplicate keys are rejected. */
@Test
fun testDuplicateKeys() {
val duplicateKeys = HexConverter.parseHexBinary("A2617805617806")
assertFailsWithMessage<DuplicateMapKeyException>("Duplicate keys not allowed in maps") {
Cbor.decodeFromByteArray<Map<String, Long>>(duplicateKeys)
}
}
}

0 comments on commit a768cf3

Please sign in to comment.