From 83f8e00612b02818b62c9173e0798637d4114f82 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Mon, 10 Aug 2020 15:31:55 +0100 Subject: [PATCH] CORDA-3936: Add a fallback mechanism for Enums incorrectly serialised using their toString() method. (#6603) * CORDA-3936: Add a fallback mechanism for Enums incorrectly serialised using their toString() method. * Backport missing piece of Enum serializer from Corda 4.6. --- .../djvm/SandboxSerializerFactoryFactory.kt | 3 +- .../corda/serialization/djvm/Serialization.kt | 8 +- .../djvm/serializers/SandboxEnumSerializer.kt | 1 + .../DeserializeRemoteCustomisedEnumTest.kt | 144 ++++++++++++++++++ .../corda/serialization/djvm/TestHelpers.kt | 28 ++++ .../internal/amqp/EnumEvolutionSerializer.kt | 9 +- .../amqp/EvolutionSerializerFactory.kt | 17 ++- .../internal/amqp/SerializerFactoryBuilder.kt | 9 +- .../WhitelistBasedTypeModelConfiguration.kt | 3 +- .../internal/model/LocalTypeInformation.kt | 3 +- .../model/LocalTypeInformationBuilder.kt | 15 +- .../internal/model/LocalTypeModel.kt | 3 +- .../serialization/internal/amqp/EnumTests.kt | 3 +- .../internal/amqp/EnumToStringFallbackTest.kt | 100 ++++++++++++ .../internal/amqp/EnvelopeHelpers.kt | 24 +++ 15 files changed, 339 insertions(+), 31 deletions(-) create mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeRemoteCustomisedEnumTest.kt create mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/TestHelpers.kt create mode 100644 serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumToStringFallbackTest.kt create mode 100644 serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnvelopeHelpers.kt diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxSerializerFactoryFactory.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxSerializerFactoryFactory.kt index bafa2b8dea..96f8c44a03 100644 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxSerializerFactoryFactory.kt +++ b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxSerializerFactoryFactory.kt @@ -98,7 +98,8 @@ class SandboxSerializerFactoryFactory( localSerializerFactory = localSerializerFactory, classLoader = classLoader, mustPreserveDataWhenEvolving = context.preventDataLoss, - primitiveTypes = primitiveTypes + primitiveTypes = primitiveTypes, + baseTypes = localTypes ) val remoteSerializerFactory = DefaultRemoteSerializerFactory( diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/Serialization.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/Serialization.kt index fdf18afe99..6b73fa6e61 100644 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/Serialization.kt +++ b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/Serialization.kt @@ -61,8 +61,9 @@ fun createSandboxSerializationEnv( @Suppress("unchecked_cast") val isEnumPredicate = predicateFactory.apply(CheckEnum::class.java) as Predicate> @Suppress("unchecked_cast") - val enumConstants = taskFactory.apply(DescribeEnum::class.java) - .andThen(taskFactory.apply(GetEnumNames::class.java)) + val enumConstants = taskFactory.apply(DescribeEnum::class.java) as Function, Array> + @Suppress("unchecked_cast") + val enumConstantNames = enumConstants.andThen(taskFactory.apply(GetEnumNames::class.java)) .andThen { (it as Array).map(Any::toString) } as Function, List> val sandboxLocalTypes = BaseLocalTypes( @@ -72,7 +73,8 @@ fun createSandboxSerializationEnv( mapClass = classLoader.toSandboxClass(Map::class.java), stringClass = classLoader.toSandboxClass(String::class.java), isEnum = isEnumPredicate, - enumConstants = enumConstants + enumConstants = enumConstants, + enumConstantNames = enumConstantNames ) val schemeBuilder = SandboxSerializationSchemeBuilder( classLoader = classLoader, diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxEnumSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxEnumSerializer.kt index a052e799da..ff1d7d75eb 100644 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxEnumSerializer.kt +++ b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxEnumSerializer.kt @@ -86,6 +86,7 @@ private class ConcreteEnumSerializer( declaredType, TypeIdentifier.forGenericType(declaredType), memberNames, + emptyMap(), emptyList(), EnumTransforms.empty ) diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeRemoteCustomisedEnumTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeRemoteCustomisedEnumTest.kt new file mode 100644 index 0000000000..7c2f5ffee7 --- /dev/null +++ b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeRemoteCustomisedEnumTest.kt @@ -0,0 +1,144 @@ +package net.corda.serialization.djvm + +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.internal._contextSerializationEnv +import net.corda.core.serialization.serialize +import net.corda.serialization.djvm.SandboxType.KOTLIN +import net.corda.serialization.internal.amqp.CompositeType +import net.corda.serialization.internal.amqp.DeserializationInput +import net.corda.serialization.internal.amqp.RestrictedType +import net.corda.serialization.internal.amqp.TypeNotation +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.fail +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import java.util.function.Function + +/** + * Corda 4.4 briefly serialised [Enum] values using [Enum.toString] rather + * than [Enum.name]. We need to be able to deserialise these values now + * that the bug has been fixed. + */ +@ExtendWith(LocalSerialization::class) +class DeserializeRemoteCustomisedEnumTest : TestBase(KOTLIN) { + @ParameterizedTest + @EnumSource(Broken::class) + fun `test deserialize broken enum with custom toString`(broken: Broken) { + val workingData = broken.serialize().rewriteEnumAsWorking() + + sandbox { + _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) + + val sandboxWorkingClass = classLoader.toSandboxClass(Working::class.java) + val sandboxWorkingValue = workingData.deserializeFor(classLoader) + assertThat(sandboxWorkingValue::class.java).isSameAs(sandboxWorkingClass) + assertThat(sandboxWorkingValue.toString()).isEqualTo(broken.label) + } + } + + /** + * This function rewrites the [SerializedBytes] for a naked [Broken] object + * into the [SerializedBytes] that Corda 4.4 would generate for an equivalent + * [Working] object. + */ + @Suppress("unchecked_cast") + private fun SerializedBytes.rewriteEnumAsWorking(): SerializedBytes { + val envelope = DeserializationInput.getEnvelope(this).apply { + val restrictedType = schema.types[0] as RestrictedType + (schema.types as MutableList)[0] = restrictedType.copy( + name = toWorking(restrictedType.name) + ) + } + return SerializedBytes(envelope.write()) + } + + @ParameterizedTest + @EnumSource(Broken::class) + fun `test deserialize composed broken enum with custom toString`(broken: Broken) { + val brokenContainer = BrokenContainer(broken) + val workingData = brokenContainer.serialize().rewriteContainerAsWorking() + + sandbox { + _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) + + val sandboxContainer = workingData.deserializeFor(classLoader) + + val taskFactory = classLoader.createRawTaskFactory() + val showWorkingData = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowWorkingData::class.java) + val result = showWorkingData.apply(sandboxContainer) ?: fail("Result cannot be null") + + assertEquals("Working: label='${broken.label}', ordinal='${broken.ordinal}'", result.toString()) + assertEquals(SANDBOX_STRING, result::class.java.name) + } + } + + class ShowWorkingData : Function { + override fun apply(input: WorkingContainer): String { + return with(input) { + "Working: label='${value.label}', ordinal='${value.ordinal}'" + } + } + } + + /** + * This function rewrites the [SerializedBytes] for a [Broken] + * property that has been composed inside a [BrokenContainer]. + * It will generate the [SerializedBytes] that Corda 4.4 would + * generate for an equivalent [WorkingContainer]. + */ + @Suppress("unchecked_cast") + private fun SerializedBytes.rewriteContainerAsWorking(): SerializedBytes { + val envelope = DeserializationInput.getEnvelope(this).apply { + val compositeType = schema.types[0] as CompositeType + (schema.types as MutableList)[0] = compositeType.copy( + name = toWorking(compositeType.name), + fields = compositeType.fields.map { it.copy(type = toWorking(it.type)) } + ) + val restrictedType = schema.types[1] as RestrictedType + (schema.types as MutableList)[1] = restrictedType.copy( + name = toWorking(restrictedType.name) + ) + } + return SerializedBytes(envelope.write()) + } + + private fun toWorking(oldName: String): String = oldName.replace("Broken", "Working") + + /** + * This is the enumerated type, as it actually exist. + */ + @Suppress("unused") + enum class Working(val label: String) { + ZERO("None"), + ONE("Once"), + TWO("Twice"); + + @Override + override fun toString(): String = label + } + + @CordaSerializable + data class WorkingContainer(val value: Working) + + /** + * This represents a broken serializer's view of the [Working] + * enumerated type, which would serialize using [Enum.toString] + * rather than [Enum.name]. + */ + @Suppress("unused") + @CordaSerializable + enum class Broken(val label: String) { + None("None"), + Once("Once"), + Twice("Twice"); + + @Override + override fun toString(): String = label + } + + @CordaSerializable + data class BrokenContainer(val value: Broken) +} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/TestHelpers.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/TestHelpers.kt new file mode 100644 index 0000000000..0d5a46d179 --- /dev/null +++ b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/TestHelpers.kt @@ -0,0 +1,28 @@ +@file:JvmName("TestHelpers") +package net.corda.serialization.djvm + +import net.corda.serialization.internal.SectionId +import net.corda.serialization.internal.amqp.Envelope +import net.corda.serialization.internal.amqp.alsoAsByteBuffer +import net.corda.serialization.internal.amqp.amqpMagic +import net.corda.serialization.internal.amqp.withDescribed +import net.corda.serialization.internal.amqp.withList +import org.apache.qpid.proton.codec.Data +import java.io.ByteArrayOutputStream + +fun Envelope.write(): ByteArray { + val data = Data.Factory.create() + data.withDescribed(Envelope.DESCRIPTOR_OBJECT) { + withList { + putObject(obj) + putObject(schema) + putObject(transformsSchema) + } + } + return ByteArrayOutputStream().use { + amqpMagic.writeTo(it) + SectionId.DATA_AND_STOP.writeTo(it) + it.alsoAsByteBuffer(data.encodedSize().toInt(), data::encode) + it.toByteArray() + } +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt index ec9ef9e678..77b57f9235 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt @@ -1,14 +1,10 @@ package net.corda.serialization.internal.amqp -import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.SerializationContext -import net.corda.serialization.internal.model.LocalTypeInformation -import org.apache.qpid.proton.amqp.Symbol +import net.corda.serialization.internal.model.BaseLocalTypes import org.apache.qpid.proton.codec.Data -import java.io.NotSerializableException import java.lang.UnsupportedOperationException import java.lang.reflect.Type -import java.util.* /** * Used whenever a deserialized enums fingerprint doesn't match the fingerprint of the generated @@ -39,6 +35,7 @@ import java.util.* class EnumEvolutionSerializer( override val type: Type, factory: LocalSerializerFactory, + private val baseLocalTypes: BaseLocalTypes, private val conversions: Map, private val ordinals: Map) : AMQPSerializer { override val typeDescriptor = factory.createDescriptor(type) @@ -51,7 +48,7 @@ class EnumEvolutionSerializer( val converted = conversions[enumName] ?: throw AMQPNotSerializableException(type, "No rule to evolve enum constant $type::$enumName") val ordinal = ordinals[converted] ?: throw AMQPNotSerializableException(type, "Ordinal not found for enum value $type::$converted") - return type.asClass().enumConstants[ordinal] + return baseLocalTypes.enumConstants.apply(type.asClass())[ordinal] } override fun writeClassInfo(output: SerializationOutput) { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerFactory.kt index 11b57b7ae3..b16a6d2213 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerFactory.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerFactory.kt @@ -40,7 +40,8 @@ class DefaultEvolutionSerializerFactory( private val localSerializerFactory: LocalSerializerFactory, private val classLoader: ClassLoader, private val mustPreserveDataWhenEvolving: Boolean, - override val primitiveTypes: Map, Class<*>> + override val primitiveTypes: Map, Class<*>>, + private val baseTypes: BaseLocalTypes ): EvolutionSerializerFactory { // Invert the "primitive -> boxed primitive" mapping. private val primitiveBoxedTypes: Map, Class<*>> @@ -154,16 +155,16 @@ class DefaultEvolutionSerializerFactory( val localTransforms = localTypeInformation.transforms val transforms = if (remoteTransforms.size > localTransforms.size) remoteTransforms else localTransforms - val localOrdinals = localTypeInformation.members.asSequence().mapIndexed { ord, member -> member to ord }.toMap() - val remoteOrdinals = members.asSequence().mapIndexed { ord, member -> member to ord }.toMap() + val localOrdinals = localTypeInformation.members.mapIndexed { ord, member -> member to ord }.toMap() + val remoteOrdinals = members.mapIndexed { ord, member -> member to ord }.toMap() val rules = transforms.defaults + transforms.renames // We just trust our transformation rules not to contain cycles here. tailrec fun findLocal(remote: String): String = - if (remote in localOrdinals) remote - else findLocal(rules[remote] ?: throw EvolutionSerializationException( - this, - "Cannot resolve local enum member $remote to a member of ${localOrdinals.keys} using rules $rules" + if (remote in localOrdinals.keys) remote + else localTypeInformation.fallbacks[remote] ?: findLocal(rules[remote] ?: throw EvolutionSerializationException( + this, + "Cannot resolve local enum member $remote to a member of ${localOrdinals.keys} using rules $rules" )) val conversions = members.associate { it to findLocal(it) } @@ -172,7 +173,7 @@ class DefaultEvolutionSerializerFactory( if (constantsAreReordered(localOrdinals, convertedOrdinals)) throw EvolutionSerializationException(this, "Constants have been reordered, additions must be appended to the end") - return EnumEvolutionSerializer(localTypeInformation.observedType, localSerializerFactory, conversions, localOrdinals) + return EnumEvolutionSerializer(localTypeInformation.observedType, localSerializerFactory, baseTypes, conversions, localOrdinals) } private fun constantsAreReordered(localOrdinals: Map, convertedOrdinals: Map): Boolean = diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt index dac25a17ad..e1d0aaee77 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt @@ -97,10 +97,8 @@ object SerializerFactoryBuilder { mustPreserveDataWhenEvolving: Boolean): SerializerFactory { val customSerializerRegistry = CachingCustomSerializerRegistry(descriptorBasedSerializerRegistry) - val localTypeModel = ConfigurableLocalTypeModel( - WhitelistBasedTypeModelConfiguration( - whitelist, - customSerializerRegistry)) + val typeModelConfiguration = WhitelistBasedTypeModelConfiguration(whitelist, customSerializerRegistry) + val localTypeModel = ConfigurableLocalTypeModel(typeModelConfiguration) val fingerPrinter = overrideFingerPrinter ?: TypeModellingFingerPrinter(customSerializerRegistry, classCarpenter.classloader) @@ -124,7 +122,8 @@ object SerializerFactoryBuilder { localSerializerFactory, classCarpenter.classloader, mustPreserveDataWhenEvolving, - javaPrimitiveTypes + javaPrimitiveTypes, + typeModelConfiguration.baseTypes ) else NoEvolutionSerializerFactory val remoteSerializerFactory = DefaultRemoteSerializerFactory( diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/WhitelistBasedTypeModelConfiguration.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/WhitelistBasedTypeModelConfiguration.kt index fe9dcbb357..2e907059aa 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/WhitelistBasedTypeModelConfiguration.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/WhitelistBasedTypeModelConfiguration.kt @@ -61,7 +61,8 @@ private val DEFAULT_BASE_TYPES = BaseLocalTypes( mapClass = Map::class.java, stringClass = String::class.java, isEnum = Predicate { clazz -> clazz.isEnum }, - enumConstants = Function { clazz -> + enumConstants = Function { clazz -> clazz.enumConstants }, + enumConstantNames = Function { clazz -> (clazz as Class>).enumConstants.map(Enum<*>::name) } ) \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformation.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformation.kt index fd1814ff80..302cb24ce4 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformation.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformation.kt @@ -36,7 +36,7 @@ typealias PropertyName = String * If a concrete type does not have a unique deserialization constructor, it is represented by [NonComposable], meaning * that we know how to take it apart but do not know how to put it back together again. * - * An array of any type is represented by [ArrayOf]. Enums are represented by [AnEnum]. + * An array of any type is represented by [AnArray]. Enums are represented by [AnEnum]. * * The type of [Any]/[java.lang.Object] is represented by [Top]. Unbounded wildcards, or wildcards whose upper bound is * [Top], are represented by [Unknown]. Bounded wildcards are always resolved to their upper bounds, e.g. @@ -178,6 +178,7 @@ sealed class LocalTypeInformation { override val observedType: Class<*>, override val typeIdentifier: TypeIdentifier, val members: List, + val fallbacks: Map, val interfaces: List, val transforms: EnumTransforms): LocalTypeInformation() diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformationBuilder.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformationBuilder.kt index add971b99a..a1aa72c0bd 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformationBuilder.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformationBuilder.kt @@ -115,13 +115,22 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup, baseTypes.mapClass.isAssignableFrom(type) -> AMap(type, typeIdentifier, Unknown, Unknown) type === baseTypes.stringClass -> Atomic(type, typeIdentifier) type.kotlin.javaPrimitiveType != null -> Atomic(type, typeIdentifier) - baseTypes.isEnum.test(type) -> baseTypes.enumConstants.apply(type).let { enumConstants -> + baseTypes.isEnum.test(type) -> baseTypes.enumConstantNames.apply(type).let { enumConstantNames -> AnEnum( type, typeIdentifier, - enumConstants, + enumConstantNames, + /** + * Calculate "fallbacks" for any [Enum] incorrectly serialised + * as its [Enum.toString] value. We are only interested in the + * cases where these are different from [Enum.name]. + * These fallbacks DO NOT contribute to this type's fingerprint. + */ + baseTypes.enumConstants.apply(type).map(Any::toString).mapIndexed { ord, fallback -> + fallback to enumConstantNames[ord] + }.filterNot { it.first == it.second }.toMap(), buildInterfaceInformation(type), - getEnumTransforms(type, enumConstants) + getEnumTransforms(type, enumConstantNames) ) } type.kotlinObjectInstance != null -> Singleton( diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeModel.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeModel.kt index 6186a09dbf..7cfdfa3cfc 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeModel.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeModel.kt @@ -136,5 +136,6 @@ class BaseLocalTypes( val mapClass: Class<*>, val stringClass: Class<*>, val isEnum: Predicate>, - val enumConstants: Function, List> + val enumConstants: Function, Array>, + val enumConstantNames: Function, List> ) diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumTests.kt index b406283d9f..0a76f19751 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumTests.kt @@ -3,7 +3,6 @@ package net.corda.serialization.internal.amqp import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes -import net.corda.core.serialization.deserialize import net.corda.serialization.internal.EmptyWhitelist import net.corda.serialization.internal.amqp.testutils.TestSerializationOutput import net.corda.serialization.internal.amqp.testutils.deserialize @@ -184,7 +183,7 @@ class EnumTests { data class C(val a: OldBras2) // DO NOT CHANGE THIS, it's important we serialise with a value that doesn't - // change position in the upated enum class + // change position in the updated enum class // Original version of the class for the serialised version of this class // diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumToStringFallbackTest.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumToStringFallbackTest.kt new file mode 100644 index 0000000000..6389f78591 --- /dev/null +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumToStringFallbackTest.kt @@ -0,0 +1,100 @@ +package net.corda.serialization.internal.amqp + +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.SerializationContext.UseCase.Testing +import net.corda.core.serialization.SerializedBytes +import net.corda.serialization.internal.AllWhitelist +import net.corda.serialization.internal.SerializationContextImpl +import net.corda.serialization.internal.amqp.testutils.TestSerializationOutput +import net.corda.serialization.internal.amqp.testutils.testDefaultFactory +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +/** + * Corda 4.4 briefly serialised [Enum] values using [Enum.toString] rather + * than [Enum.name]. We need to be able to deserialise these values now + * that the bug has been fixed. + */ +class EnumToStringFallbackTest { + private lateinit var serializationOutput: TestSerializationOutput + + private fun createTestContext(): SerializationContext = SerializationContextImpl( + preferredSerializationVersion = amqpMagic, + deserializationClassLoader = ClassLoader.getSystemClassLoader(), + whitelist = AllWhitelist, + properties = emptyMap(), + objectReferencesEnabled = false, + useCase = Testing, + encoding = null + ) + + @Before + fun setup() { + serializationOutput = TestSerializationOutput(verbose = false) + } + + @Test(timeout = 300_000) + fun deserializeEnumWithToString() { + val broken = BrokenContainer(Broken.Twice) + val brokenData = serializationOutput.serialize(broken, createTestContext()) + val workingData = brokenData.rewriteAsWorking() + val working = DeserializationInput(testDefaultFactory()).deserialize(workingData, createTestContext()) + assertEquals(Working.TWO, working.value) + } + + /** + * This function rewrites the [SerializedBytes] for a [Broken] + * property that has been composed inside a [BrokenContainer]. + * It will generate the [SerializedBytes] that Corda 4.4 would + * generate for an equivalent [WorkingContainer]. + */ + @Suppress("unchecked_cast") + private fun SerializedBytes.rewriteAsWorking(): SerializedBytes { + val envelope = DeserializationInput.getEnvelope(this).apply { + val compositeType = schema.types[0] as CompositeType + (schema.types as MutableList)[0] = compositeType.copy( + name = toWorking(compositeType.name), + fields = compositeType.fields.map { it.copy(type = toWorking(it.type)) } + ) + val restrictedType = schema.types[1] as RestrictedType + (schema.types as MutableList)[1] = restrictedType.copy( + name = toWorking(restrictedType.name) + ) + } + return SerializedBytes(envelope.write()) + } + + private fun toWorking(oldName: String): String = oldName.replace("Broken", "Working") + + /** + * This is the enumerated type, as it actually exist. + */ + @Suppress("unused") + enum class Working(private val label: String) { + ZERO("None"), + ONE("Once"), + TWO("Twice"); + + @Override + override fun toString(): String = label + } + + /** + * This represents a broken serializer's view of the [Working] + * enumerated type, which would serialize using [Enum.toString] + * rather than [Enum.name]. + */ + @Suppress("unused") + enum class Broken(private val label: String) { + None("None"), + Once("Once"), + Twice("Twice"); + + @Override + override fun toString(): String = label + } + + data class WorkingContainer(val value: Working) + data class BrokenContainer(val value: Broken) +} \ No newline at end of file diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnvelopeHelpers.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnvelopeHelpers.kt new file mode 100644 index 0000000000..4b46ad2183 --- /dev/null +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnvelopeHelpers.kt @@ -0,0 +1,24 @@ +@file:JvmName("EnvelopeHelpers") +package net.corda.serialization.internal.amqp + +import net.corda.serialization.internal.SectionId +import net.corda.serialization.internal.amqp.Envelope.Companion.DESCRIPTOR_OBJECT +import org.apache.qpid.proton.codec.Data +import java.io.ByteArrayOutputStream + +fun Envelope.write(): ByteArray { + val data = Data.Factory.create() + data.withDescribed(DESCRIPTOR_OBJECT) { + withList { + putObject(obj) + putObject(schema) + putObject(transformsSchema) + } + } + return ByteArrayOutputStream().use { + amqpMagic.writeTo(it) + SectionId.DATA_AND_STOP.writeTo(it) + it.alsoAsByteBuffer(data.encodedSize().toInt(), data::encode) + it.toByteArray() + } +} \ No newline at end of file