From 1785c5134a8303e03c9725f7fb518bd155e2f31d Mon Sep 17 00:00:00 2001 From: xiaozhikang Date: Sun, 17 Nov 2024 20:12:26 +0800 Subject: [PATCH 1/6] introduce ProtoUnknownFields --- .../protobuf/ProtoUnknownFields.kt | 131 ++++++++++++++++++ .../protobuf/internal/Helpers.kt | 24 +++- .../internal/ProtoMessageSerializer.kt | 90 ++++++++++++ .../protobuf/internal/ProtobufDecoding.kt | 60 ++++++-- .../protobuf/internal/ProtobufEncoding.kt | 33 +++++ .../protobuf/internal/ProtobufReader.kt | 10 ++ .../protobuf/internal/ProtobufTaggedBase.kt | 4 +- .../protobuf/ProtobufUnknownFieldsTest.kt | 112 +++++++++++++++ 8 files changed, 446 insertions(+), 18 deletions(-) create mode 100644 formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/ProtoUnknownFields.kt create mode 100644 formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtoMessageSerializer.kt create mode 100644 formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufUnknownFieldsTest.kt diff --git a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/ProtoUnknownFields.kt b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/ProtoUnknownFields.kt new file mode 100644 index 0000000000..cba50d0383 --- /dev/null +++ b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/ProtoUnknownFields.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.protobuf + +import kotlinx.serialization.* +import kotlinx.serialization.protobuf.internal.* +import kotlinx.serialization.protobuf.internal.ProtoWireType + +/** + * Mark a property as a holder for unknown fields in protobuf message. + */ +@SerialInfo +@Target(AnnotationTarget.PROPERTY) +@ExperimentalSerializationApi +public annotation class ProtoUnknownFields + +@Serializable(with = ProtoMessageSerializer::class) +public class ProtoMessage internal constructor( + public val fields: List +) { + public companion object { + public val Empty: ProtoMessage = ProtoMessage(emptyList()) + } + + public val size: Int get() = fields.size + public fun asByteArray(): ByteArray = fields.fold(ByteArray(0)) { acc, protoField -> acc + protoField.asWireContent() } + + public constructor(vararg fields: ProtoField) : this(fields.toList()) + public fun merge(other: ProtoMessage): ProtoMessage { + return ProtoMessage(fields + other.fields) + } + + public fun merge(vararg field: ProtoField): ProtoMessage { + return ProtoMessage(fields + field) + } + + override fun hashCode(): Int { + return fields.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as ProtoMessage + + return fields == other.fields + } +} + +@OptIn(ExperimentalSerializationApi::class) +@Serializable(with = ProtoFieldSerializer::class) +@KeepGeneratedSerializer +@ConsistentCopyVisibility +public data class ProtoField internal constructor( + internal val id: Int, + internal val wireType: ProtoWireType, + internal val data: ProtoContentHolder +) { + public companion object { + public val Empty: ProtoField = ProtoField(0, ProtoWireType.INVALID, ProtoContentHolder.ByteArrayContent(ByteArray(0))) + } + + public fun asWireContent(): ByteArray = byteArrayOf(((id shl 3) or wireType.typeId).toByte()) + data.byteArray + + public val contentLength: Int + get() = asWireContent().size + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as ProtoField + + if (id != other.id) return false + if (wireType != other.wireType) return false + if (!data.contentEquals(other.data)) return false + + return true + } + + override fun hashCode(): Int { + var result = id + result = 31 * result + wireType.hashCode() + result = 31 * result + data.contentHashCode() + return result + } +} + +internal sealed interface ProtoContentHolder { + val byteArray: ByteArray + + data class ByteArrayContent(override val byteArray: ByteArray) : ProtoContentHolder { + override fun equals(other: Any?): Boolean { + return other is ProtoContentHolder && this.contentEquals(other) + } + + override fun hashCode(): Int { + return this.contentHashCode() + } + } + + data class MessageContent(val content: ProtoMessage) : ProtoContentHolder { + override val byteArray: ByteArray + get() = content.asByteArray() + + override fun equals(other: Any?): Boolean { + return other is ProtoContentHolder && this.contentEquals(other) + } + + override fun hashCode(): Int { + return this.contentHashCode() + } + } +} + +internal fun ProtoContentHolder(content: ByteArray): ProtoContentHolder = ProtoContentHolder.ByteArrayContent(content) + +internal val ProtoContentHolder.contentLength: Int + get() = byteArray.size + +internal fun ProtoContentHolder.contentEquals(other: ProtoContentHolder): Boolean { + return byteArray.contentEquals(other.byteArray) +} + +internal fun ProtoContentHolder.contentHashCode(): Int { + return byteArray.contentHashCode() +} + diff --git a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/Helpers.kt b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/Helpers.kt index ea6d4b6823..fed5536755 100644 --- a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/Helpers.kt +++ b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/Helpers.kt @@ -37,16 +37,25 @@ internal enum class ProtoWireType(val typeId: Int) { } internal const val ID_HOLDER_ONE_OF = -2 +internal const val ID_HOLDER_UNKNOWN_FIELDS = -3 +private const val UNKNOWN_FIELD_MASK = 1L shl 37 private const val ONEOFMASK = 1L shl 36 private const val INTTYPEMASK = 3L shl 33 private const val PACKEDMASK = 1L shl 32 @Suppress("NOTHING_TO_INLINE") -internal inline fun ProtoDesc(protoId: Int, type: ProtoIntegerType, packed: Boolean = false, oneOf: Boolean = false): ProtoDesc { +internal inline fun ProtoDesc( + protoId: Int, + type: ProtoIntegerType, + packed: Boolean = false, + oneOf: Boolean = false, + unknown: Boolean = false, +): ProtoDesc { val packedBits = if (packed) PACKEDMASK else 0L val oneOfBits = if (oneOf) ONEOFMASK else 0L - return packedBits or oneOfBits or type.signature or protoId.toLong() + val unknownBits = if (unknown) UNKNOWN_FIELD_MASK else 0L + return packedBits or oneOfBits or type.signature or protoId.toLong() or unknownBits } internal inline val ProtoDesc.protoId: Int get() = (this and Int.MAX_VALUE.toLong()).toInt() @@ -72,6 +81,9 @@ internal val ProtoDesc.isPacked: Boolean internal val ProtoDesc.isOneOf: Boolean get() = (this and ONEOFMASK) != 0L +internal val ProtoDesc.isUnknown: Boolean + get() = (this and UNKNOWN_FIELD_MASK) != 0L + internal fun ProtoDesc.overrideId(protoId: Int): ProtoDesc { return this and (0xFFFFFFF00000000L) or protoId.toLong() } @@ -82,6 +94,7 @@ internal fun SerialDescriptor.extractParameters(index: Int): ProtoDesc { var format: ProtoIntegerType = ProtoIntegerType.DEFAULT var protoPacked = false var isOneOf = false + var isUnknown = false for (i in annotations.indices) { // Allocation-friendly loop val annotation = annotations[i] @@ -94,6 +107,8 @@ internal fun SerialDescriptor.extractParameters(index: Int): ProtoDesc { protoPacked = true } else if (annotation is ProtoOneOf) { isOneOf = true + } else if (annotation is ProtoUnknownFields) { + isUnknown = true } } if (isOneOf) { @@ -102,7 +117,7 @@ internal fun SerialDescriptor.extractParameters(index: Int): ProtoDesc { // See [kotlinx.serialization.protobuf.internal.ProtobufDecoder.decodeElementIndex] for detail protoId = index + 1 } - return ProtoDesc(protoId, format, protoPacked, isOneOf) + return ProtoDesc(protoId, format, protoPacked, isOneOf, isUnknown) } /** @@ -117,6 +132,9 @@ internal fun extractProtoId(descriptor: SerialDescriptor, index: Int, zeroBasedD if (annotation is ProtoOneOf) { // Fast return for one of field return ID_HOLDER_ONE_OF + } else if (annotation is ProtoUnknownFields) { + // Fast return for unknown fields holder + return ID_HOLDER_UNKNOWN_FIELDS } else if (annotation is ProtoNumber) { result = annotation.number // 0 or negative numbers are acceptable for enums diff --git a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtoMessageSerializer.kt b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtoMessageSerializer.kt new file mode 100644 index 0000000000..5fa4d8faec --- /dev/null +++ b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtoMessageSerializer.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.protobuf.internal + +import kotlinx.serialization.* +import kotlinx.serialization.builtins.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* +import kotlinx.serialization.protobuf.* + +internal object ProtoMessageSerializer : KSerializer { + internal val fieldsSerializer = ProtoFieldSerializer + + override val descriptor: SerialDescriptor + get() = UnknownFieldsDescriptor(fieldsSerializer.descriptor) + + override fun deserialize(decoder: Decoder): ProtoMessage { + if (decoder is ProtobufDecoder) { + return decoder.decodeStructure(descriptor) { + ProtoMessage(fieldsSerializer.deserializeComposite(this)) + } + } + return ProtoMessage.Empty + } + + override fun serialize(encoder: Encoder, value: ProtoMessage) { + if (encoder is ProtobufEncoder) { + value.fields.forEach { + fieldsSerializer.serialize(encoder, it) + } + } + } +} + +internal object ProtoFieldSerializer : KSerializer { + private val delegate = ByteArraySerializer() + + override val descriptor: SerialDescriptor + get() = UnknownFieldsDescriptor(delegate.descriptor) + + fun deserializeComposite(compositeDecoder: CompositeDecoder): ProtoField { + if (compositeDecoder is ProtobufDecoder) { + return deserialize(compositeDecoder) + } + return ProtoField.Empty + } + + override fun deserialize(decoder: Decoder): ProtoField { + if (decoder is ProtobufDecoder) { + return deserialize(decoder, decoder.currentTag) + } + return ProtoField.Empty + } + + internal fun deserialize(protobufDecoder: ProtobufDecoder, currentTag: ProtoDesc): ProtoField { + if (currentTag != MISSING_TAG) { + val id = currentTag.protoId + val type = protobufDecoder.currentType + val data = protobufDecoder.decodeRawElement() + val field = ProtoField( + id = id, + wireType = type, + data = ProtoContentHolder(data), + ) + return field + } + return ProtoField.Empty + } + + override fun serialize(encoder: Encoder, value: ProtoField) { + if (encoder is ProtobufEncoder) { + encoder.encodeRawElement(value.id, value.wireType, value.data.byteArray) + } + } +} + +internal class UnknownFieldsDescriptor(private val original: SerialDescriptor) : SerialDescriptor by original { + override val serialName: String + get() = "UnknownProtoFieldsHolder" + + override fun equals(other: Any?): Boolean { + return other is UnknownFieldsDescriptor && other.original == original + } + + override fun hashCode(): Int { + return original.hashCode() + } +} diff --git a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt index 56884b12a7..d9740a5d03 100644 --- a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt +++ b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt @@ -31,9 +31,14 @@ internal open class ProtobufDecoder( // Index -> proto id for oneof element. An oneof element of certain index may refer to different proto id in runtime. private var index2IdMap: MutableMap? = null + private var unknownHolderIndex: Int = -1 + private var nullValue: Boolean = false private val elementMarker = ElementMarker(descriptor, ::readIfAbsent) + internal val currentType: ProtoWireType + get() = reader.currentType + init { populateCache(descriptor) } @@ -52,10 +57,10 @@ internal open class ProtobufDecoder( val cache = IntArray(elements + 1) { -1 } for (i in 0 until elements) { val protoId = extractProtoId(descriptor, i, false) - // If any element is marked as ProtoOneOf, + // If any element is marked as ProtoOneOf on Unknown field holder, // the fast path is not applicable - // because it will contain more id than elements - if (protoId <= elements && protoId != ID_HOLDER_ONE_OF) { + // because num of id does not match the elements + if (protoId in 0..elements) { cache[protoId] = i } else { return populateCacheMap(descriptor, elements) @@ -72,14 +77,24 @@ internal open class ProtobufDecoder( var oneOfCount = 0 for (i in 0 until elements) { val id = extractProtoId(descriptor, i, false) - if (id == ID_HOLDER_ONE_OF) { - descriptor.getElementDescriptor(i) - .getAllOneOfSerializerOfField(serializersModule) - .map { it.extractParameters(0).protoId } - .forEach { map.putProtoId(it, i) } - oneOfCount ++ - } else { - map.putProtoId(extractProtoId(descriptor, i, false), i) + when (id) { + ID_HOLDER_ONE_OF -> { + descriptor.getElementDescriptor(i) + .getAllOneOfSerializerOfField(serializersModule) + .map { it.extractParameters(0).protoId } + .forEach { map.putProtoId(it, i) } + oneOfCount ++ + } + ID_HOLDER_UNKNOWN_FIELDS -> { + require(unknownHolderIndex == -1) { + "Only one unknown fields holder is allowed in a message" + } + oneOfCount ++ + unknownHolderIndex = i + } + else -> { + map.putProtoId(id, i) + } } } if (oneOfCount > 0) { @@ -102,7 +117,7 @@ internal open class ProtobufDecoder( private fun getIndexByNumSlowPath( protoTag: Int - ): Int = sparseIndexCache!!.getOrElse(protoTag) { -1 } + ): Int = sparseIndexCache!!.getOrElse(protoTag) { unknownHolderIndex } private fun findIndexByTag(descriptor: SerialDescriptor, protoTag: Int): Int { // Fast-path: tags are incremental, 1-based @@ -252,6 +267,9 @@ internal open class ProtobufDecoder( deserializer.descriptor == ByteArraySerializer().descriptor -> deserializeByteArray(previousValue as ByteArray?) as T deserializer is AbstractCollectionSerializer<*, *, *> -> (deserializer as AbstractCollectionSerializer<*, T, *>).merge(this, previousValue) + deserializer == ProtoMessageSerializer -> { + decodeUnknownFields(previousValue as? ProtoMessage) as T + } else -> deserializer.deserialize(this) } @@ -317,7 +335,8 @@ internal open class ProtobufDecoder( if (index == -1) { // not found reader.skipElement() } else { - if (descriptor.extractParameters(index).isOneOf) { + val tag = descriptor.extractParameters(index) + if (tag.isOneOf || tag.isUnknown) { /** * While decoding message with one-of field, * the proto id read from wire data cannot be easily found @@ -356,6 +375,21 @@ internal open class ProtobufDecoder( return false } + private fun decodeUnknownFields(previous: ProtoMessage?): ProtoMessage { + require(currentTagOrDefault != MISSING_TAG) { + "Cannot deserialize directly to kotlinx.serialization.protobuf.ProtoMessage." + } + val serializer = ProtoFieldSerializer + val restoredTag = index2IdMap?.get(unknownHolderIndex)?.let { currentTag.overrideId(it) } ?: currentTag + return serializer.deserialize(this, restoredTag).let { + previous?.merge(it) ?: ProtoMessage(it) + } + } + + internal fun decodeRawElement(): ByteArray { + return reader.readRawElement() + } + private inline fun decodeOrThrow(tag: ProtoDesc, action: (tag: ProtoDesc) -> T): T { try { return action(tag) diff --git a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufEncoding.kt b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufEncoding.kt index b7d5dd28e5..edacc6f295 100644 --- a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufEncoding.kt +++ b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufEncoding.kt @@ -142,10 +142,36 @@ internal open class ProtobufEncoder( serializer is MapLikeSerializer<*, *, *, *> -> { serializeMap(serializer as SerializationStrategy, value) } + serializer == ProtoMessageSerializer -> { + serializeUnknownFields(serializer as ProtoMessageSerializer, value as ProtoMessage) + } serializer.descriptor == ByteArraySerializer().descriptor -> serializeByteArray(value as ByteArray) else -> serializer.serialize(this, value) } + internal fun encodeRawElement(id: Int, wireType: ProtoWireType, data: ByteArray) { + when(wireType) { + ProtoWireType.INVALID -> {} + ProtoWireType.VARINT -> writer.writeInt( + value = data.first().toInt(), + tag = id, + format = ProtoIntegerType.DEFAULT + ) + + ProtoWireType.i64 -> writer.writeLong( + value = data.first().toLong(), + tag = id, + format = ProtoIntegerType.FIXED + ) + ProtoWireType.SIZE_DELIMITED -> writer.writeBytes(data, id) + ProtoWireType.i32 -> writer.writeInt( + value = data.first().toInt(), + tag = id, + format = ProtoIntegerType.FIXED + ) + } + } + private fun serializeByteArray(value: ByteArray) { val tag = popTagOrDefault() if (tag == MISSING_TAG) { @@ -155,6 +181,13 @@ internal open class ProtobufEncoder( } } + private fun serializeUnknownFields(serializer: SerializationStrategy, protoMessage: ProtoMessage) { + require(currentTagOrDefault != MISSING_TAG) { + "Cannot serialize directly from kotlinx.serialization.protobuf.ProtoMessage." + } + serializer.serialize(this, protoMessage) + } + @Suppress("UNCHECKED_CAST") private fun serializeMap(serializer: SerializationStrategy, value: T) { // encode maps as collection of map entries, not merged collection of key-values diff --git a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufReader.kt b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufReader.kt index 5b8ce1c292..f8b6a5ea45 100644 --- a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufReader.kt +++ b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufReader.kt @@ -65,6 +65,16 @@ internal class ProtobufReader(private val input: ByteArrayInput) { } } + fun readRawElement(): ByteArray { + return when (currentType) { + ProtoWireType.VARINT -> byteArrayOf(readInt(ProtoIntegerType.DEFAULT).toByte()) + ProtoWireType.i64 -> byteArrayOf(readLong(ProtoIntegerType.FIXED).toByte()) + ProtoWireType.SIZE_DELIMITED -> readByteArray() + ProtoWireType.i32 -> byteArrayOf(readInt(ProtoIntegerType.FIXED).toByte()) + else -> throw ProtobufDecodingException("Unsupported start group or end group wire type: $currentType") + } + } + @Suppress("NOTHING_TO_INLINE") private inline fun assertWireType(expected: ProtoWireType) { if (currentType != expected) throw ProtobufDecodingException("Expected wire type $expected, but found $currentType") diff --git a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedBase.kt b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedBase.kt index ffa9f47940..f9282dc3cf 100644 --- a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedBase.kt +++ b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedBase.kt @@ -28,10 +28,10 @@ internal abstract class ProtobufTaggedBase { @JvmField protected var stackSize = -1 - protected val currentTag: ProtoDesc + internal val currentTag: ProtoDesc get() = tagsStack[stackSize] - protected val currentTagOrDefault: ProtoDesc + internal val currentTagOrDefault: ProtoDesc get() = if (stackSize == -1) MISSING_TAG else tagsStack[stackSize] protected fun popTagOrDefault(): ProtoDesc = if (stackSize == -1) MISSING_TAG else tagsStack[stackSize--] diff --git a/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufUnknownFieldsTest.kt b/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufUnknownFieldsTest.kt new file mode 100644 index 0000000000..b8b9add349 --- /dev/null +++ b/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufUnknownFieldsTest.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.protobuf + +import kotlinx.serialization.* +import kotlin.test.* + +class ProtobufUnknownFieldsTest { + @Serializable + data class InnerData(val name: String, val b: Int, val c: List) + @Serializable + data class BuildData(val a: Int, val b: String, val c: ByteArray, val d: List, val e: InnerData) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as BuildData + + if (a != other.a) return false + if (b != other.b) return false + if (!c.contentEquals(other.c)) return false + if (d != other.d) return false + if (e != other.e) return false + + return true + } + + override fun hashCode(): Int { + var result = a + result = 31 * result + b.hashCode() + result = 31 * result + c.contentHashCode() + result = 31 * result + d.hashCode() + result = 31 * result + e.hashCode() + return result + } + + } + + @Serializable + data class DataWithUnknownFields( + val a: Int, + @ProtoUnknownFields val unknownFields: ProtoMessage + ) + + @Test + fun testDecodeWithUnknownField() { + val data = BuildData(42, "42", byteArrayOf(42, 42, 42), listOf(42, 42, 42), InnerData("42", 42, listOf("42", "42", "42"))) + val encoded = "082a120234321a032a2a2a202a202a202a2a120a023432102a1a0234321a0234321a023432" + val decoded = ProtoBuf.decodeFromHexString(DataWithUnknownFields.serializer(), encoded) + assertEquals(data.a, decoded.a) + assertEquals(6, decoded.unknownFields.size) + + val encoded2 = ProtoBuf.encodeToHexString(DataWithUnknownFields.serializer(), decoded) + assertEquals(encoded, encoded2) + val data2 = ProtoBuf.decodeFromHexString(BuildData.serializer(), encoded2) + assertEquals(data, data2) + } + + @Test + fun testCannotDecodeArbitraryMessage() { + assertFailsWith { + ProtoBuf.decodeFromHexString(ProtoMessage.serializer(), "") + } + } + + @Test + fun testCannotEncodeArbitraryMessage() { + assertFailsWith { + ProtoBuf.encodeToHexString(ProtoMessage.serializer(), ProtoMessage.Empty) + } + } + + @Serializable + data class DataWithMultipleUnknownFields( + val a: Int, + @ProtoUnknownFields val unknownFields: ProtoMessage, + @ProtoUnknownFields val unknownFields2: ProtoMessage + ) + + @Test + fun testOnlyOneUnknownFieldAllowed() { + val encoded = "082a120234321a032a2a2a202a202a202a2a120a023432102a1a0234321a0234321a023432" + assertFailsWith { + ProtoBuf.decodeFromHexString(DataWithMultipleUnknownFields.serializer(), encoded) + } + } + + @Serializable + data class DataWithStaggeredFields( + @ProtoNumber(2) + val b: String, + @ProtoUnknownFields val unknownFields: ProtoMessage, + @ProtoNumber(4) + val d: List + ) + + @Test + fun testUnknownFieldBeforeKnownField() { + val data = BuildData(42, "42", byteArrayOf(42, 42, 42), listOf(42, 42, 42), InnerData("42", 42, listOf("42", "42", "42"))) + val hex = "082a120234321a032a2a2a202a202a202a2a120a023432102a1a0234321a0234321a023432" + val decoded = ProtoBuf.decodeFromHexString(DataWithStaggeredFields.serializer(), hex) + assertEquals(3, decoded.unknownFields.size) + assertEquals("42", decoded.b) + assertEquals(listOf(42, 42, 42), decoded.d) + + val encoded = ProtoBuf.encodeToHexString(DataWithStaggeredFields.serializer(), decoded) + val decodeOrigin = ProtoBuf.decodeFromHexString(BuildData.serializer(), encoded) + assertEquals(data, decodeOrigin) + } +} \ No newline at end of file From 7bdceee07a4cfdb36026ba35c022817cc951f940 Mon Sep 17 00:00:00 2001 From: xiaozhikang Date: Sun, 17 Nov 2024 22:11:37 +0800 Subject: [PATCH 2/6] add some test --- .../protobuf/ProtoUnknownFields.kt | 18 +++ .../protobuf/internal/ProtobufDecoding.kt | 4 +- .../protobuf/ProtobufUnknownFieldsTest.kt | 143 ++++++++++++++++++ 3 files changed, 162 insertions(+), 3 deletions(-) diff --git a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/ProtoUnknownFields.kt b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/ProtoUnknownFields.kt index cba50d0383..c8d2ae5ce6 100644 --- a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/ProtoUnknownFields.kt +++ b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/ProtoUnknownFields.kt @@ -28,6 +28,9 @@ public class ProtoMessage internal constructor( public fun asByteArray(): ByteArray = fields.fold(ByteArray(0)) { acc, protoField -> acc + protoField.asWireContent() } public constructor(vararg fields: ProtoField) : this(fields.toList()) + + public operator fun plus(other: ProtoMessage): ProtoMessage = merge(other) + public fun merge(other: ProtoMessage): ProtoMessage { return ProtoMessage(fields + other.fields) } @@ -50,6 +53,21 @@ public class ProtoMessage internal constructor( } } +public fun ProtoMessage?.merge(other: ProtoMessage?): ProtoMessage { + return when { + this == null -> other ?: ProtoMessage.Empty + other == null -> this + else -> this + other + } +} + +public fun ProtoMessage?.merge(vararg fields: ProtoField): ProtoMessage { + return when { + this == null -> ProtoMessage(fields.toList()) + else -> this.merge(ProtoMessage(fields.toList())) + } +} + @OptIn(ExperimentalSerializationApi::class) @Serializable(with = ProtoFieldSerializer::class) @KeepGeneratedSerializer diff --git a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt index d9740a5d03..f4924e3e29 100644 --- a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt +++ b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt @@ -381,9 +381,7 @@ internal open class ProtobufDecoder( } val serializer = ProtoFieldSerializer val restoredTag = index2IdMap?.get(unknownHolderIndex)?.let { currentTag.overrideId(it) } ?: currentTag - return serializer.deserialize(this, restoredTag).let { - previous?.merge(it) ?: ProtoMessage(it) - } + return previous.merge(serializer.deserialize(this, restoredTag)) } internal fun decodeRawElement(): ByteArray { diff --git a/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufUnknownFieldsTest.kt b/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufUnknownFieldsTest.kt index b8b9add349..2d171e13ba 100644 --- a/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufUnknownFieldsTest.kt +++ b/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufUnknownFieldsTest.kt @@ -5,6 +5,8 @@ package kotlinx.serialization.protobuf import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* import kotlin.test.* class ProtobufUnknownFieldsTest { @@ -47,6 +49,22 @@ class ProtobufUnknownFieldsTest { @Test fun testDecodeWithUnknownField() { val data = BuildData(42, "42", byteArrayOf(42, 42, 42), listOf(42, 42, 42), InnerData("42", 42, listOf("42", "42", "42"))) + + /** + * 1: 42 + * 2: {"42"} + * 3: {"***"} + * 4: 42 + * 4: 42 + * 4: 42 + * 5: { + * 1: {"42"} + * 2: 42 + * 3: {"42"} + * 3: {"42"} + * 3: {"42"} + * } + */ val encoded = "082a120234321a032a2a2a202a202a202a2a120a023432102a1a0234321a0234321a023432" val decoded = ProtoBuf.decodeFromHexString(DataWithUnknownFields.serializer(), encoded) assertEquals(data.a, decoded.a) @@ -99,6 +117,23 @@ class ProtobufUnknownFieldsTest { @Test fun testUnknownFieldBeforeKnownField() { val data = BuildData(42, "42", byteArrayOf(42, 42, 42), listOf(42, 42, 42), InnerData("42", 42, listOf("42", "42", "42"))) + + /** + * 1: 42 + * 2: {"42"} + * 3: {"***"} + * 4: 42 + * 4: 42 + * 4: 42 + * 5: { + * 1: {"42"} + * 2: 42 + * 3: {"42"} + * 3: {"42"} + * 3: {"42"} + * } + * } + */ val hex = "082a120234321a032a2a2a202a202a202a2a120a023432102a1a0234321a0234321a023432" val decoded = ProtoBuf.decodeFromHexString(DataWithStaggeredFields.serializer(), hex) assertEquals(3, decoded.unknownFields.size) @@ -106,7 +141,115 @@ class ProtobufUnknownFieldsTest { assertEquals(listOf(42, 42, 42), decoded.d) val encoded = ProtoBuf.encodeToHexString(DataWithStaggeredFields.serializer(), decoded) + /** + * fields are re-ordered but acceptable in protobuf wire data + * + * 2: {"42"} + * 1: 42 + * 3: {"***"} + * 5: { + * 1: {"42"} + * 2: 42 + * 3: {"42"} + * 3: {"42"} + * 3: {"42"} + * } + * 4: 42 + * 4: 42 + * 4: 42 + */ + assertEquals("12023432082a1a032a2a2a2a120a023432102a1a0234321a0234321a023432202a202a202a", encoded) val decodeOrigin = ProtoBuf.decodeFromHexString(BuildData.serializer(), encoded) assertEquals(data, decodeOrigin) } + + @Serializable + data class TotalKnownData(@ProtoUnknownFields val fields: ProtoMessage = ProtoMessage.Empty) + + @Serializable + data class NestedUnknownData(val a: Int, @ProtoNumber(5) val inner: TotalKnownData, @ProtoUnknownFields val unknown: ProtoMessage) + + @Test + fun testDecodeNestedUnknownData() { + /** + * 1: 42 + * 2: {"42"} + * 3: {"***"} + * 4: 42 + * 4: 42 + * 4: 42 + * 5: { + * 1: {"42"} + * 2: 42 + * 3: {"42"} + * 3: {"42"} + * 3: {"42"} + * } + */ + val hex = "082a120234321a032a2a2a202a202a202a2a120a023432102a1a0234321a0234321a023432" + val decoded = ProtoBuf.decodeFromHexString(NestedUnknownData.serializer(), hex) + assertEquals(5, decoded.unknown.size) + } + + object CustomSerializer: KSerializer { + override val descriptor: SerialDescriptor + get() = buildClassSerialDescriptor("CustomData") { + element("a", annotations = listOf(ProtoNumber(1))) + element("unknownFields", annotations = listOf(ProtoUnknownFields())) + } + + override fun deserialize(decoder: Decoder): DataWithUnknownFields { + var a = 0 + var unknownFields = ProtoMessage.Empty + decoder.decodeStructure(descriptor) { + loop@ while (true) { + when (val index = decodeElementIndex(descriptor)) { + CompositeDecoder.DECODE_DONE -> break@loop + 0 -> a = decodeIntElement(descriptor, index) + 1 -> unknownFields += decodeSerializableElement(descriptor, index, ProtoMessage.serializer()) + else -> error("Unexpected index: $index") + } + } + } + return DataWithUnknownFields(a, unknownFields) + } + + override fun serialize(encoder: Encoder, value: DataWithUnknownFields) { + encoder.encodeStructure(descriptor) { + encodeIntElement(descriptor, 0, value.a) + encodeSerializableElement(descriptor, 1, ProtoMessage.serializer(), value.unknownFields) + } + } + } + + @Test + fun testCustomSerializer() { + val data = BuildData(42, "42", byteArrayOf(42, 42, 42), listOf(42, 42, 42), InnerData("42", 42, listOf("42", "42", "42"))) + + /** + * 1: 42 + * 2: {"42"} + * 3: {"***"} + * 4: 42 + * 4: 42 + * 4: 42 + * 5: { + * 1: {"42"} + * 2: 42 + * 3: {"42"} + * 3: {"42"} + * 3: {"42"} + * } + */ + val encoded = "082a120234321a032a2a2a202a202a202a2a120a023432102a1a0234321a0234321a023432" + val decoded = ProtoBuf.decodeFromHexString(CustomSerializer, encoded) + + assertEquals(data.a, decoded.a) + assertEquals(6, decoded.unknownFields.size) + + val encoded2 = ProtoBuf.encodeToHexString(CustomSerializer, decoded) + assertEquals(encoded, encoded2) + val data2 = ProtoBuf.decodeFromHexString(BuildData.serializer(), encoded2) + assertEquals(data, data2) + } } \ No newline at end of file From cfcdddb4ccf5d79f8a00e20374e356773563b681 Mon Sep 17 00:00:00 2001 From: xiaozhikang Date: Sun, 17 Nov 2024 22:55:32 +0800 Subject: [PATCH 3/6] comments --- .../protobuf/ProtoUnknownFields.kt | 85 ++++++++++++++++--- .../protobuf/internal/ProtobufDecoding.kt | 24 +++--- 2 files changed, 87 insertions(+), 22 deletions(-) diff --git a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/ProtoUnknownFields.kt b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/ProtoUnknownFields.kt index c8d2ae5ce6..64a86e2e8e 100644 --- a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/ProtoUnknownFields.kt +++ b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/ProtoUnknownFields.kt @@ -9,33 +9,62 @@ import kotlinx.serialization.protobuf.internal.* import kotlinx.serialization.protobuf.internal.ProtoWireType /** - * Mark a property as a holder for unknown fields in protobuf message. + * Mark a property with type [ProtoMessage] as a holder for unknown fields in protobuf message. + * + * All the contents with unregistered proto number will be stored in this field. */ @SerialInfo @Target(AnnotationTarget.PROPERTY) @ExperimentalSerializationApi public annotation class ProtoUnknownFields +/** + * Represents a protobuf message. + * + * Especially used as a holder of unknown proto fields in an arbitrary protobuf message. + */ @Serializable(with = ProtoMessageSerializer::class) public class ProtoMessage internal constructor( - public val fields: List + internal val fields: List ) { public companion object { + /** + * An empty [ProtoMessage] instance. + * + * Useful as a default value for [ProtoUnknownFields] properties. + */ public val Empty: ProtoMessage = ProtoMessage(emptyList()) } + /** + * Number of fields holding in the message. + */ public val size: Int get() = fields.size - public fun asByteArray(): ByteArray = fields.fold(ByteArray(0)) { acc, protoField -> acc + protoField.asWireContent() } - public constructor(vararg fields: ProtoField) : this(fields.toList()) + /** + * Returns a byte array representing of the message. + */ + public fun asByteArray(): ByteArray = + fields.fold(ByteArray(0)) { acc, protoField -> acc + protoField.asWireContent() } + + internal constructor(vararg fields: ProtoField) : this(fields.toList()) + /** + * Merges two [ProtoMessage] instances. + */ public operator fun plus(other: ProtoMessage): ProtoMessage = merge(other) + /** + * Merges two [ProtoMessage] instances. + */ public fun merge(other: ProtoMessage): ProtoMessage { return ProtoMessage(fields + other.fields) } - public fun merge(vararg field: ProtoField): ProtoMessage { + /** + * Convenience method to merge multiple [ProtoField] with this message. + */ + internal fun merge(vararg field: ProtoField): ProtoMessage { return ProtoMessage(fields + field) } @@ -53,6 +82,9 @@ public class ProtoMessage internal constructor( } } +/** + * Convenience method to merge two nullable [ProtoMessage] instances. + */ public fun ProtoMessage?.merge(other: ProtoMessage?): ProtoMessage { return when { this == null -> other ?: ProtoMessage.Empty @@ -61,29 +93,35 @@ public fun ProtoMessage?.merge(other: ProtoMessage?): ProtoMessage { } } -public fun ProtoMessage?.merge(vararg fields: ProtoField): ProtoMessage { +/** + * Convenience method to merge multiple [ProtoField] with a nullable [ProtoMessage]. + */ +internal fun ProtoMessage?.merge(vararg fields: ProtoField): ProtoMessage { return when { this == null -> ProtoMessage(fields.toList()) else -> this.merge(ProtoMessage(fields.toList())) } } +/** + * Represents a single field in a protobuf message. + */ @OptIn(ExperimentalSerializationApi::class) @Serializable(with = ProtoFieldSerializer::class) @KeepGeneratedSerializer @ConsistentCopyVisibility -public data class ProtoField internal constructor( +internal data class ProtoField internal constructor( internal val id: Int, internal val wireType: ProtoWireType, internal val data: ProtoContentHolder ) { - public companion object { - public val Empty: ProtoField = ProtoField(0, ProtoWireType.INVALID, ProtoContentHolder.ByteArrayContent(ByteArray(0))) + companion object { + val Empty: ProtoField = ProtoField(0, ProtoWireType.INVALID, ProtoContentHolder.ByteArrayContent(ByteArray(0))) } - public fun asWireContent(): ByteArray = byteArrayOf(((id shl 3) or wireType.typeId).toByte()) + data.byteArray + fun asWireContent(): ByteArray = byteArrayOf(((id shl 3) or wireType.typeId).toByte()) + data.byteArray - public val contentLength: Int + val contentLength: Int get() = asWireContent().size override fun equals(other: Any?): Boolean { @@ -107,9 +145,19 @@ public data class ProtoField internal constructor( } } +/** + * A data representation of a protobuf field in [ProtoField.data], without the field number and wire type. + */ internal sealed interface ProtoContentHolder { + + /** + * Returns a byte array representation of the content. + */ val byteArray: ByteArray + /** + * Represents the content in raw byte array. + */ data class ByteArrayContent(override val byteArray: ByteArray) : ProtoContentHolder { override fun equals(other: Any?): Boolean { return other is ProtoContentHolder && this.contentEquals(other) @@ -120,6 +168,9 @@ internal sealed interface ProtoContentHolder { } } + /** + * Represents the content with a nested [ProtoMessage]. + */ data class MessageContent(val content: ProtoMessage) : ProtoContentHolder { override val byteArray: ByteArray get() = content.asByteArray() @@ -134,15 +185,27 @@ internal sealed interface ProtoContentHolder { } } +/** + * Creates a [ProtoContentHolder] instance with a byte array content. + */ internal fun ProtoContentHolder(content: ByteArray): ProtoContentHolder = ProtoContentHolder.ByteArrayContent(content) +/** + * Get the length in bytes of the content in the [ProtoContentHolder]. + */ internal val ProtoContentHolder.contentLength: Int get() = byteArray.size +/** + * Checks if the content of two [ProtoContentHolder] instances are equal. + */ internal fun ProtoContentHolder.contentEquals(other: ProtoContentHolder): Boolean { return byteArray.contentEquals(other.byteArray) } +/** + * Calculates the hash code of the content in the [ProtoContentHolder]. + */ internal fun ProtoContentHolder.contentHashCode(): Int { return byteArray.contentHashCode() } diff --git a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt index f4924e3e29..61979e8f5f 100644 --- a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt +++ b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt @@ -28,7 +28,8 @@ internal open class ProtobufDecoder( private var indexCache: IntArray? = null private var sparseIndexCache: MutableMap? = null - // Index -> proto id for oneof element. An oneof element of certain index may refer to different proto id in runtime. + // Index -> proto id for oneof element or unknown fields. + // These kind of elements in certain index may refer to different proto id in runtime. private var index2IdMap: MutableMap? = null private var unknownHolderIndex: Int = -1 @@ -57,7 +58,7 @@ internal open class ProtobufDecoder( val cache = IntArray(elements + 1) { -1 } for (i in 0 until elements) { val protoId = extractProtoId(descriptor, i, false) - // If any element is marked as ProtoOneOf on Unknown field holder, + // If any element is marked as ProtoOneOf or Unknown field holder, // the fast path is not applicable // because num of id does not match the elements if (protoId in 0..elements) { @@ -74,7 +75,7 @@ internal open class ProtobufDecoder( private fun populateCacheMap(descriptor: SerialDescriptor, elements: Int) { val map = HashMap(elements, 1f) - var oneOfCount = 0 + var mapSize = 0 for (i in 0 until elements) { val id = extractProtoId(descriptor, i, false) when (id) { @@ -83,13 +84,13 @@ internal open class ProtobufDecoder( .getAllOneOfSerializerOfField(serializersModule) .map { it.extractParameters(0).protoId } .forEach { map.putProtoId(it, i) } - oneOfCount ++ + mapSize ++ } ID_HOLDER_UNKNOWN_FIELDS -> { require(unknownHolderIndex == -1) { "Only one unknown fields holder is allowed in a message" } - oneOfCount ++ + mapSize ++ unknownHolderIndex = i } else -> { @@ -97,8 +98,8 @@ internal open class ProtobufDecoder( } } } - if (oneOfCount > 0) { - index2IdMap = HashMap(oneOfCount, 1f) + if (mapSize > 0) { + index2IdMap = HashMap(mapSize, 1f) } sparseIndexCache = map } @@ -338,12 +339,13 @@ internal open class ProtobufDecoder( val tag = descriptor.extractParameters(index) if (tag.isOneOf || tag.isUnknown) { /** - * While decoding message with one-of field, + * While decoding message with one-of field or unknown fields, * the proto id read from wire data cannot be easily found * in the properties of this type, - * So the index of this one-of property and the id read from the wire - * are saved in this map, then restored in [beginStructure] - * and passed to [OneOfPolymorphicReader] to get the actual deserializer. + * So the index of this property and the id read from the wire + * are saved in this map, then + * 1. restored in [beginStructure] and passed to [OneOfPolymorphicReader] to get the actual deserializer, or + * 2. restored in [decodeUnknownFields] to get the right proto id for the unknown fields. */ index2IdMap?.put(index, protoId) } From 310c9c430bc7f849a729ad9c7313d1dea48ad44d Mon Sep 17 00:00:00 2001 From: xiaozhikang Date: Mon, 18 Nov 2024 11:26:23 +0800 Subject: [PATCH 4/6] docs --- docs/formats.md | 71 +++++++-- docs/serialization-guide.md | 1 + ...{ProtoUnknownFields.kt => ProtoMessage.kt} | 13 -- .../serialization/protobuf/ProtoTypes.kt | 12 +- .../protobuf/internal/ProtobufDecoding.kt | 3 + .../protobuf/internal/ProtobufEncoding.kt | 3 + .../protobuf/ProtobufUnknownFieldsTest.kt | 46 ++++++ guide/example/example-formats-09.kt | 26 +++- guide/example/example-formats-10.kt | 19 +-- guide/example/example-formats-11.kt | 35 +---- guide/example/example-formats-12.kt | 31 +--- guide/example/example-formats-13.kt | 6 +- guide/example/example-formats-14.kt | 24 +-- guide/example/example-formats-15.kt | 12 +- guide/example/example-formats-16.kt | 78 ++++------ guide/example/example-formats-17.kt | 47 +----- guide/example/example-formats-18.kt | 142 ++++++++++++++++++ guide/test/FormatsTest.kt | 42 +++--- 18 files changed, 374 insertions(+), 237 deletions(-) rename formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/{ProtoUnknownFields.kt => ProtoMessage.kt} (92%) create mode 100644 guide/example/example-formats-18.kt diff --git a/docs/formats.md b/docs/formats.md index 307601234d..081af3a848 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -25,6 +25,7 @@ stable, these are currently experimental features of Kotlin Serialization. * [Oneof field (experimental)](#oneof-field-experimental) * [Usage](#usage) * [Alternative](#alternative) + * [Preserving unknown fields (experimental)](#preserving-unknown-fields-experimental) * [ProtoBuf schema generator (experimental)](#protobuf-schema-generator-experimental) * [Properties (experimental)](#properties-experimental) * [Custom formats (experimental)](#custom-formats-experimental) @@ -483,7 +484,7 @@ Field #3: 1D Fixed32 Value = 3, Hex = 03-00-00-00 ### Lists as repeated fields -By default, kotlin lists and other collections are representend as repeated fields. +By default, kotlin lists and other collections are represented as repeated fields. In the protocol buffers when the list is empty there are no elements in the stream with the corresponding number. For Kotlin Serialization you must explicitly specify a default of `emptyList()` for any property of a collection or map type. Otherwise you will not be able deserialize an empty @@ -642,6 +643,54 @@ is also compatible with the `message Data` given above, which means the same inp But please note that there are no exclusivity checks. This means that if an instance of `Data2` has both (or none) `homeNumber` and `workNumber` as non-null values and is serialized to protobuf, it no longer complies with the original schema. If you send such data to another parser, one of the fields may be omitted, leading to an unknown issue. +### Preserving unknown fields (experimental) + +You may keep updating your schema by adding new fields, but you may not want to break compatibility with the old data. + +Kotlin Serialization `ProtoBuf` format supports preserving unknown fields, as described in the [Protocol Buffer-Unknown Fields](https://protobuf.dev/programming-guides/proto3/#unknowns). + +To keep the unknown fields, add a property in type `ProtoMessage` with default value `null` or `ProtoMessage.Empty`, and annotation `@ProtoUnknownFields` to your data class. + + + +```kotlin +@OptIn(ExperimentalSerializationApi::class) +@Serializable +data class Data( + @ProtoNumber(1) val name: String, + @ProtoUnknownFields val unknownFields: ProtoMessage = ProtoMessage.Empty +) + +@OptIn(ExperimentalSerializationApi::class) +@Serializable +data class NewData( + @ProtoNumber(1) val name: String, + @ProtoNumber(2) val age: Int, +) + +@OptIn(ExperimentalSerializationApi::class) +fun main() { + val dataFromNewBinary = NewData("Tom", 25) + val hexString = ProtoBuf.encodeToHexString(dataFromNewBinary) + val dataInOldBinary = ProtoBuf.decodeFromHexString(hexString) + val hexOfOldData = ProtoBuf.encodeToHexString(dataInOldBinary) + println(hexOfOldData) + println(hexString) + assert(hexOfOldData == hexString) +} +``` + +> You can get the full code [here](../guide/example/example-formats-09.kt). + +```text +0a03546f6d1019 +0a03546f6d1019 +``` + + ### ProtoBuf schema generator (experimental) As mentioned above, when working with protocol buffers you usually use a ".proto" file and a code generator for your @@ -676,7 +725,7 @@ fun main() { println(schemas) } ``` -> You can get the full code [here](../guide/example/example-formats-09.kt). +> You can get the full code [here](../guide/example/example-formats-10.kt). Which would output as follows. @@ -684,7 +733,7 @@ Which would output as follows. syntax = "proto2"; -// serial name 'example.exampleFormats09.SampleData' +// serial name 'example.exampleFormats10.SampleData' message SampleData { required int64 amount = 1; optional string description = 2; @@ -729,7 +778,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-formats-10.kt). +> You can get the full code [here](../guide/example/example-formats-11.kt). The resulting map has dot-separated keys representing keys of the nested objects. @@ -814,7 +863,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-formats-11.kt). +> You can get the full code [here](../guide/example/example-formats-12.kt). As a result, we got all the primitive values in our object graph visited and put into a list in _serial_ order. @@ -923,7 +972,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-formats-12.kt). +> You can get the full code [here](../guide/example/example-formats-13.kt). Now we can convert a list of primitives back to an object tree. @@ -1021,7 +1070,7 @@ fun main() { } --> -> You can get the full code [here](../guide/example/example-formats-13.kt). +> You can get the full code [here](../guide/example/example-formats-14.kt).