From afe3280d4466eefa7ab921cc34962d38057c848e Mon Sep 17 00:00:00 2001 From: KosmX Date: Fri, 12 Sep 2025 22:10:12 +0000 Subject: [PATCH 1/3] New tests for packed unsigned encoding (failing for now) --- .../protobuf/PackedArraySerializerTest.kt | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/PackedArraySerializerTest.kt b/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/PackedArraySerializerTest.kt index 630602b13..74b185e95 100644 --- a/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/PackedArraySerializerTest.kt +++ b/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/PackedArraySerializerTest.kt @@ -2,6 +2,8 @@ * Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +@file:OptIn(ExperimentalUnsignedTypes::class) + package kotlinx.serialization.protobuf import kotlinx.serialization.* @@ -57,6 +59,31 @@ class PackedArraySerializerTest { val i: List ) + @Serializable + data class PackedUByteCarrier( + @ProtoPacked + val b: UByteArray + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as PackedUByteCarrier + + return b.contentEquals(other.b) + } + + override fun hashCode(): Int { + return b.contentHashCode() + } + } + + @Serializable + data class PackedUintCarrier( + @ProtoPacked + val i: List + ) + /** * Test that when packing is specified the array is encoded as packed */ @@ -164,4 +191,33 @@ class PackedArraySerializerTest { assertEquals(obj, decodedAbsentField) } + @Test + fun testDecodePackedUnsigned() { + val expectedByteCarrier = PackedUByteCarrier(ubyteArrayOf(1.toUByte(), 2.toUByte(), 128.toUByte())) + val expectedIntCarrier = PackedUintCarrier(listOf(1u, 2u, 3u, 128u)) + + val byteHex = """0a03010280""" + val intHex = """0a050102038001""" + + val decodedBytes = ProtoBuf.decodeFromHexString(byteHex) + val decodedInts = ProtoBuf.decodeFromHexString(intHex) + + assertEquals(expectedByteCarrier, decodedBytes) + assertEquals(expectedIntCarrier, decodedInts) + } + @Test + fun testEncodePackedUnsigned() { + val byteCarrier = PackedUByteCarrier(ubyteArrayOf(1.toUByte(), 2.toUByte(), 128.toUByte())) + val intCarrier = PackedUintCarrier(listOf(1u, 2u, 3u, 128u)) + + val expectedByteHex = """0a03010280""" + val expectedIntHex = """0a050102038001""" + + val byteHex = ProtoBuf.encodeToHexString(byteCarrier) + val intHex = ProtoBuf.encodeToHexString(intCarrier) + + assertEquals(expectedByteHex, byteHex) + assertEquals(expectedIntHex, intHex) + } + } From 1f9f7931b24925ba5805170cbb548ee301d6900b Mon Sep 17 00:00:00 2001 From: KosmX Date: Fri, 12 Sep 2025 23:27:26 +0000 Subject: [PATCH 2/3] Add UByteArray as "bytes" serializer, just like ByteArray --- .../kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt | 2 ++ .../kotlinx/serialization/protobuf/internal/ProtobufEncoding.kt | 2 ++ 2 files changed, 4 insertions(+) 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 56884b12a..2014702f7 100644 --- a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt +++ b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt @@ -242,6 +242,7 @@ internal open class ProtobufDecoder( override fun decodeSerializableValue(deserializer: DeserializationStrategy): T = decodeSerializableValue(deserializer, null) + @OptIn(ExperimentalUnsignedTypes::class) @Suppress("UNCHECKED_CAST") override fun decodeSerializableValue(deserializer: DeserializationStrategy, previousValue: T?): T = try { when { @@ -250,6 +251,7 @@ internal open class ProtobufDecoder( } deserializer.descriptor == ByteArraySerializer().descriptor -> deserializeByteArray(previousValue as ByteArray?) as T + deserializer.descriptor == UByteArraySerializer().descriptor -> deserializeByteArray((previousValue as UByteArray?)?.asByteArray()).asUByteArray() as T deserializer is AbstractCollectionSerializer<*, *, *> -> (deserializer as AbstractCollectionSerializer<*, T, *>).merge(this, previousValue) 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 b7d5dd28e..6f47e326d 100644 --- a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufEncoding.kt +++ b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufEncoding.kt @@ -138,11 +138,13 @@ internal open class ProtobufEncoder( override fun SerialDescriptor.getTag(index: Int) = extractParameters(index) + @OptIn(ExperimentalUnsignedTypes::class) override fun encodeSerializableValue(serializer: SerializationStrategy, value: T) = when { serializer is MapLikeSerializer<*, *, *, *> -> { serializeMap(serializer as SerializationStrategy, value) } serializer.descriptor == ByteArraySerializer().descriptor -> serializeByteArray(value as ByteArray) + serializer.descriptor == UByteArraySerializer().descriptor -> serializeByteArray((value as UByteArray).asByteArray()) else -> serializer.serialize(this, value) } From a120db377f57c6c1f59620d885d4f4095fd65c6a Mon Sep 17 00:00:00 2001 From: KosmX Date: Fri, 12 Sep 2025 23:29:03 +0000 Subject: [PATCH 3/3] Make sure that primitives and inline value classes that are primitives can be packed. inline popTag -> popTagOrDefault is needed, because packed serializer will use MISSING_TAG for entries, popTag fails on that. --- .../src/kotlinx/serialization/protobuf/internal/Helpers.kt | 3 +-- .../serialization/protobuf/internal/ProtobufTaggedDecoder.kt | 2 +- .../serialization/protobuf/internal/ProtobufTaggedEncoder.kt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) 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 33e1c78cc..f9fe2dc78 100644 --- a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/Helpers.kt +++ b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/Helpers.kt @@ -68,12 +68,11 @@ internal val ProtoDesc.integerType: ProtoIntegerType } internal val SerialDescriptor.isPackable: Boolean - @OptIn(kotlinx.serialization.ExperimentalSerializationApi::class) get() = when (kind) { PrimitiveKind.STRING, !is PrimitiveKind -> false else -> true - } + } || isInline && elementsCount == 1 && getElementDescriptor(0).isPackable internal val ProtoDesc.isPacked: Boolean get() = (this and PACKEDMASK) != 0L diff --git a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedDecoder.kt b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedDecoder.kt index 953c1b3ce..7cf3172c2 100644 --- a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedDecoder.kt +++ b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedDecoder.kt @@ -95,7 +95,7 @@ internal abstract class ProtobufTaggedDecoder : ProtobufTaggedBase(), Decoder, C } override fun decodeInline(descriptor: SerialDescriptor): Decoder { - return decodeTaggedInline(popTag(), descriptor) + return decodeTaggedInline(popTagOrDefault(), descriptor) } override fun decodeInlineElement( diff --git a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedEncoder.kt b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedEncoder.kt index 6ba93e9e7..99ab2a1d2 100644 --- a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedEncoder.kt +++ b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedEncoder.kt @@ -175,7 +175,7 @@ internal abstract class ProtobufTaggedEncoder : ProtobufTaggedBase(), Encoder, C } override fun encodeInline(descriptor: SerialDescriptor): Encoder { - return encodeTaggedInline(popTag(), descriptor) + return encodeTaggedInline(popTagOrDefault(), descriptor) } override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder {