mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Merge commit '83f8e00612b02818b62c9173e0798637d4114f82' into chrisr3-os44-os45-merge
This commit is contained in:
commit
d4bedfd1d5
@ -86,6 +86,7 @@ private class ConcreteEnumSerializer(
|
||||
declaredType,
|
||||
TypeIdentifier.forGenericType(declaredType),
|
||||
memberNames,
|
||||
emptyMap(),
|
||||
emptyList(),
|
||||
EnumTransforms.empty
|
||||
)
|
||||
|
@ -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<Broken>.rewriteEnumAsWorking(): SerializedBytes<Working> {
|
||||
val envelope = DeserializationInput.getEnvelope(this).apply {
|
||||
val restrictedType = schema.types[0] as RestrictedType
|
||||
(schema.types as MutableList<TypeNotation>)[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<WorkingContainer, String> {
|
||||
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<BrokenContainer>.rewriteContainerAsWorking(): SerializedBytes<WorkingContainer> {
|
||||
val envelope = DeserializationInput.getEnvelope(this).apply {
|
||||
val compositeType = schema.types[0] as CompositeType
|
||||
(schema.types as MutableList<TypeNotation>)[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<TypeNotation>)[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)
|
||||
}
|
@ -155,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) }
|
||||
|
@ -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<String>,
|
||||
val fallbacks: Map<String, String>,
|
||||
val interfaces: List<LocalTypeInformation>,
|
||||
val transforms: EnumTransforms): LocalTypeInformation()
|
||||
|
||||
|
@ -120,6 +120,15 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
|
||||
type,
|
||||
typeIdentifier,
|
||||
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, enumConstantNames)
|
||||
)
|
||||
|
@ -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
|
||||
//
|
||||
|
@ -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<BrokenContainer>.rewriteAsWorking(): SerializedBytes<WorkingContainer> {
|
||||
val envelope = DeserializationInput.getEnvelope(this).apply {
|
||||
val compositeType = schema.types[0] as CompositeType
|
||||
(schema.types as MutableList<TypeNotation>)[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<TypeNotation>)[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)
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user