Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit a768cf3

Browse files
committedMay 19, 2024
Disallow repeated keys in CBOR (WIP, need to add config switches)
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.
1 parent 194a188 commit a768cf3

File tree

5 files changed

+50
-1
lines changed

5 files changed

+50
-1
lines changed
 

‎core/commonMain/src/kotlinx/serialization/SerializationExceptions.kt

+7
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,10 @@ internal constructor(message: String?) : SerializationException(message) {
133133
// This constructor is used by the generated serializers
134134
constructor(index: Int) : this("An unknown field for index $index")
135135
}
136+
137+
/**
138+
* Thrown when a map deserializer encounters a repeated map key (and configuration disallows this.)
139+
*/
140+
@ExperimentalSerializationApi
141+
public class DuplicateMapKeyException(public val key: Any?) :
142+
SerializationException("Duplicate keys not allowed in maps. Key appeared twice: $key")

‎core/commonMain/src/kotlinx/serialization/encoding/Decoding.kt

+12-1
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ internal inline fun <T : Any> Decoder.decodeIfNullable(deserializer: Deserializa
275275
* [CompositeDecoder] is a part of decoding process that is bound to a particular structured part of
276276
* the serialized form, described by the serial descriptor passed to [Decoder.beginStructure].
277277
*
278-
* Typically, for unordered data, [CompositeDecoder] is used by a serializer withing a [decodeElementIndex]-based
278+
* Typically, for unordered data, [CompositeDecoder] is used by a serializer within a [decodeElementIndex]-based
279279
* loop that decodes all the required data one-by-one in any order and then terminates by calling [endStructure].
280280
* Please refer to [decodeElementIndex] for example of such loop.
281281
*
@@ -558,6 +558,17 @@ public interface CompositeDecoder {
558558
deserializer: DeserializationStrategy<T?>,
559559
previousValue: T? = null
560560
): T?
561+
562+
/**
563+
* Called after a key has been read.
564+
*
565+
* This could be a map or set key, or anything otherwise intended to be
566+
* distinct within the collection under normal circumstances.
567+
*
568+
* Implementations might use this as a hook for throwing an exception when
569+
* duplicate keys are encountered.
570+
*/
571+
public fun visitKey(key: Any?) { }
561572
}
562573

563574
/**

‎core/commonMain/src/kotlinx/serialization/internal/CollectionSerializers.kt

+1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ public sealed class MapLikeSerializer<Key, Value, Collection, Builder : MutableM
9898

9999
final override fun readElement(decoder: CompositeDecoder, index: Int, builder: Builder, checkIndex: Boolean) {
100100
val key: Key = decoder.decodeSerializableElement(descriptor, index, keySerializer)
101+
decoder.visitKey(key)
101102
val vIndex = if (checkIndex) {
102103
decoder.decodeElementIndex(descriptor).also {
103104
require(it == index + 1) { "Value must follow key in a map, index for key: $index, returned index for value: $it" }

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

+10
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,17 @@ internal class CborEncoder(private val output: ByteArrayOutput) {
198198
}
199199

200200
private class CborMapReader(cbor: Cbor, decoder: CborDecoder) : CborListReader(cbor, decoder) {
201+
/** Keys that have been seen so far while reading this map. */
202+
private val seenKeys = mutableSetOf<Any?>()
203+
201204
override fun skipBeginToken() = setSize(decoder.startMap() * 2)
205+
206+
override fun visitKey(key: Any?) {
207+
val added = seenKeys.add(key)
208+
if (!added) {
209+
throw DuplicateMapKeyException(key)
210+
}
211+
}
202212
}
203213

204214
private open class CborListReader(cbor: Cbor, decoder: CborDecoder) : CborReader(cbor, decoder) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package kotlinx.serialization.cbor
2+
3+
import kotlinx.serialization.assertFailsWithMessage
4+
import kotlinx.serialization.decodeFromByteArray
5+
import kotlinx.serialization.HexConverter
6+
import kotlinx.serialization.DuplicateMapKeyException
7+
import kotlin.test.Test
8+
import kotlin.test.assertEquals
9+
10+
class CborStrictModeTest {
11+
12+
/** Duplicate keys are rejected. */
13+
@Test
14+
fun testDuplicateKeys() {
15+
val duplicateKeys = HexConverter.parseHexBinary("A2617805617806")
16+
assertFailsWithMessage<DuplicateMapKeyException>("Duplicate keys not allowed in maps") {
17+
Cbor.decodeFromByteArray<Map<String, Long>>(duplicateKeys)
18+
}
19+
}
20+
}

0 commit comments

Comments
 (0)