Merge commit '83f8e00612b02818b62c9173e0798637d4114f82' into chrisr3-os44-os45-merge

This commit is contained in:
Chris Rankin 2020-08-10 16:50:35 +01:00
commit d4bedfd1d5
8 changed files with 287 additions and 9 deletions

View File

@ -86,6 +86,7 @@ private class ConcreteEnumSerializer(
declaredType,
TypeIdentifier.forGenericType(declaredType),
memberNames,
emptyMap(),
emptyList(),
EnumTransforms.empty
)

View File

@ -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)
}

View File

@ -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) }

View File

@ -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()

View File

@ -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)
)

View File

@ -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
//

View File

@ -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)
}

View File

@ -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()
}
}