From 4e0378de9cb2ac46589f0b3f8754f4c82798569c Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Thu, 24 May 2018 18:26:55 +0100 Subject: [PATCH] CORDA-1238: Moved the blob inspector out of experimental and wired it to JackonSupport (#3224) The existing output format was not complete and so was deleted to avoid it becoming a tech debt. We can always resurrect it at a later point. --- .idea/compiler.xml | 4 +- build.gradle | 1 + client/jackson/build.gradle | 5 +- .../corda/client/jackson/JacksonSupport.kt | 128 ++++-- .../client/jackson/internal/CordaModule.kt | 39 +- .../client/jackson/JacksonSupportTest.kt | 73 +++- .../net/corda/core/internal/InternalUtils.kt | 11 + .../core/serialization/SerializationAPI.kt | 22 +- docs/source/blob-inspector.rst | 63 +++ docs/source/changelog.rst | 15 +- docs/source/tools-index.rst | 1 + experimental/blobinspector/build.gradle | 52 --- .../net/corda/blobinspector/BlobInspector.kt | 405 ------------------ .../net/corda/blobinspector/BlobLoader.kt | 40 -- .../kotlin/net/corda/blobinspector/Config.kt | 137 ------ .../kotlin/net/corda/blobinspector/Errors.kt | 3 - .../blobinspector/IndentingStringBuilder.kt | 44 -- .../kotlin/net/corda/blobinspector/Main.kt | 81 ---- .../net/corda/blobinspector/FileParseTests.kt | 87 ---- .../net/corda/blobinspector/InMemoryTests.kt | 91 ---- .../net/corda/blobinspector/ModeParse.kt | 83 ---- .../corda/blobinspector/SimplifyClassTests.kt | 28 -- .../blobinspector/FileParseTests.1Composite | Bin 537 -> 0 bytes .../corda/blobinspector/FileParseTests.1Int | Bin 244 -> 0 bytes .../blobinspector/FileParseTests.1String | Bin 262 -> 0 bytes .../blobinspector/FileParseTests.2Composite | Bin 1004 -> 0 bytes .../corda/blobinspector/FileParseTests.2Int | Bin 284 -> 0 bytes .../corda/blobinspector/FileParseTests.3Int | Bin 317 -> 0 bytes .../blobinspector/FileParseTests.IntList | Bin 426 -> 0 bytes .../blobinspector/FileParseTests.MapIntClass | Bin 759 -> 0 bytes .../blobinspector/FileParseTests.MapIntString | Bin 474 -> 0 bytes .../blobinspector/FileParseTests.StringList | Bin 448 -> 0 bytes .../net/corda/blobinspector/networkParams | Bin 3066 -> 0 bytes node/build.gradle | 2 +- .../serialization/kryo/CordaClassResolver.kt | 5 +- .../internal/amqp/SerializationHelper.kt | 56 +-- settings.gradle | 1 + tools/blobinspector/build.gradle | 27 ++ .../kotlin/net/corda/blobinspector/Main.kt | 125 ++++++ 39 files changed, 461 insertions(+), 1168 deletions(-) create mode 100644 docs/source/blob-inspector.rst delete mode 100644 experimental/blobinspector/build.gradle delete mode 100644 experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt delete mode 100644 experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobLoader.kt delete mode 100644 experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/Config.kt delete mode 100644 experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/Errors.kt delete mode 100644 experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/IndentingStringBuilder.kt delete mode 100644 experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/Main.kt delete mode 100644 experimental/blobinspector/src/test/kotlin/net/corda/blobinspector/FileParseTests.kt delete mode 100644 experimental/blobinspector/src/test/kotlin/net/corda/blobinspector/InMemoryTests.kt delete mode 100644 experimental/blobinspector/src/test/kotlin/net/corda/blobinspector/ModeParse.kt delete mode 100644 experimental/blobinspector/src/test/kotlin/net/corda/blobinspector/SimplifyClassTests.kt delete mode 100644 experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.1Composite delete mode 100644 experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.1Int delete mode 100644 experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.1String delete mode 100644 experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.2Composite delete mode 100644 experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.2Int delete mode 100644 experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.3Int delete mode 100644 experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.IntList delete mode 100644 experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.MapIntClass delete mode 100644 experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.MapIntString delete mode 100644 experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.StringList delete mode 100644 experimental/blobinspector/src/test/resources/net/corda/blobinspector/networkParams create mode 100644 tools/blobinspector/build.gradle create mode 100644 tools/blobinspector/src/main/kotlin/net/corda/blobinspector/Main.kt diff --git a/.idea/compiler.xml b/.idea/compiler.xml index af6261dd62..9632eef3de 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -71,8 +71,6 @@ - - @@ -173,6 +171,8 @@ + + diff --git a/build.gradle b/build.gradle index bcd3f867d7..ea49fc62ba 100644 --- a/build.gradle +++ b/build.gradle @@ -79,6 +79,7 @@ buildscript { ext.protonj_version = '0.27.1' ext.snappy_version = '0.4' ext.fast_classpath_scanner_version = '2.12.3' + ext.jcabi_manifests_version = '1.1' // Update 121 is required for ObjectInputFilter and at time of writing 131 was latest: ext.java8_minUpdateVersion = '131' diff --git a/client/jackson/build.gradle b/client/jackson/build.gradle index 9c59b0ee99..be0b324ed2 100644 --- a/client/jackson/build.gradle +++ b/client/jackson/build.gradle @@ -6,11 +6,8 @@ apply plugin: 'com.jfrog.artifactory' dependencies { compile project(':serialization') - testCompile project(':test-utils') compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" - // Jackson and its plugins: parsing to/from JSON and other textual formats. compile "com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version" // Yaml is useful for parsing strings to method calls. @@ -19,7 +16,9 @@ dependencies { compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version" compile "com.google.guava:guava:$guava_version" + testCompile project(':test-utils') testCompile project(path: ':core', configuration: 'testArtifacts') + testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testCompile "junit:junit:$junit_version" } diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt index 701092ef03..d360997fbf 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt @@ -21,10 +21,7 @@ import net.corda.core.contracts.Amount import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateRef import net.corda.core.crypto.* -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.AnonymousParty -import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.Party +import net.corda.core.identity.* import net.corda.core.internal.CertRole import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.uncheckedCast @@ -55,12 +52,13 @@ import javax.security.auth.x500.X500Principal * * Note that Jackson can also be used to serialise/deserialise other formats such as Yaml and XML. */ -@Suppress("DEPRECATION") +@Suppress("DEPRECATION", "MemberVisibilityCanBePrivate") object JacksonSupport { // If you change this API please update the docs in the docsite (json.rst) @DoNotImplement interface PartyObjectMapper { + val isFullParties: Boolean fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? fun partyFromKey(owningKey: PublicKey): Party? fun partiesFromName(query: String): Set @@ -68,9 +66,11 @@ object JacksonSupport { } @Deprecated("This is an internal class, do not use", replaceWith = ReplaceWith("JacksonSupport.createDefaultMapper")) - class RpcObjectMapper(val rpc: CordaRPCOps, - factory: JsonFactory, - val fuzzyIdentityMatch: Boolean) : PartyObjectMapper, ObjectMapper(factory) { + class RpcObjectMapper + @JvmOverloads constructor(val rpc: CordaRPCOps, + factory: JsonFactory, + val fuzzyIdentityMatch: Boolean, + override val isFullParties: Boolean = false) : PartyObjectMapper, ObjectMapper(factory) { override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = rpc.wellKnownPartyFromX500Name(name) override fun partyFromKey(owningKey: PublicKey): Party? = rpc.partyFromKey(owningKey) override fun partiesFromName(query: String) = rpc.partiesFromName(query, fuzzyIdentityMatch) @@ -78,9 +78,11 @@ object JacksonSupport { } @Deprecated("This is an internal class, do not use") - class IdentityObjectMapper(val identityService: IdentityService, - factory: JsonFactory, - val fuzzyIdentityMatch: Boolean) : PartyObjectMapper, ObjectMapper(factory) { + class IdentityObjectMapper + @JvmOverloads constructor(val identityService: IdentityService, + factory: JsonFactory, + val fuzzyIdentityMatch: Boolean, + override val isFullParties: Boolean = false) : PartyObjectMapper, ObjectMapper(factory) { override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = identityService.wellKnownPartyFromX500Name(name) override fun partyFromKey(owningKey: PublicKey): Party? = identityService.partyFromKey(owningKey) override fun partiesFromName(query: String) = identityService.partiesFromName(query, fuzzyIdentityMatch) @@ -88,7 +90,9 @@ object JacksonSupport { } @Deprecated("This is an internal class, do not use", replaceWith = ReplaceWith("JacksonSupport.createNonRpcMapper")) - class NoPartyObjectMapper(factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) { + class NoPartyObjectMapper + @JvmOverloads constructor(factory: JsonFactory, + override val isFullParties: Boolean = false) : PartyObjectMapper, ObjectMapper(factory) { override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = null override fun partyFromKey(owningKey: PublicKey): Party? = null override fun partiesFromName(query: String): Set = emptySet() @@ -102,22 +106,33 @@ object JacksonSupport { /** * Creates a Jackson ObjectMapper that uses RPC to deserialise parties from string names. * - * If [fuzzyIdentityMatch] is false, fields mapped to [Party] objects must be in X.500 name form and precisely + * @param fuzzyIdentityMatch If false, fields mapped to [Party] objects must be in X.500 name form and precisely * match an identity known from the network map. If true, the name is matched more leniently but if the match * is ambiguous a [JsonParseException] is thrown. + * + * @param fullParties If true then [Party] objects will be serialised as JSON objects, with the owning key serialised + * in addition to the name. For [PartyAndCertificate] objects the cert path will be included. */ @JvmStatic @JvmOverloads fun createDefaultMapper(rpc: CordaRPCOps, factory: JsonFactory = JsonFactory(), - fuzzyIdentityMatch: Boolean = false): ObjectMapper { - return configureMapper(RpcObjectMapper(rpc, factory, fuzzyIdentityMatch)) + fuzzyIdentityMatch: Boolean = false, + fullParties: Boolean = false): ObjectMapper { + return configureMapper(RpcObjectMapper(rpc, factory, fuzzyIdentityMatch, fullParties)) } - /** For testing or situations where deserialising parties is not required */ + /** + * For testing or situations where deserialising parties is not required + * + * @param fullParties If true then [Party] objects will be serialised as JSON objects, with the owning key serialised + * in addition to the name. For [PartyAndCertificate] objects the cert path will be included. + */ @JvmStatic @JvmOverloads - fun createNonRpcMapper(factory: JsonFactory = JsonFactory()): ObjectMapper = configureMapper(NoPartyObjectMapper(factory)) + fun createNonRpcMapper(factory: JsonFactory = JsonFactory(), fullParties: Boolean = false): ObjectMapper { + return configureMapper(NoPartyObjectMapper(factory, fullParties)) + } /** * Creates a Jackson ObjectMapper that uses an [IdentityService] directly inside the node to deserialise parties from string names. @@ -197,7 +212,14 @@ object JacksonSupport { .filter { Modifier.isStatic(it.modifiers) && it.type == KeyPurposeId::class.java } .associateBy({ (it.get(null) as KeyPurposeId).id }, { it.name }) - val knownExtensions = setOf("2.5.29.15", "2.5.29.37", "2.5.29.19", "2.5.29.17", "2.5.29.18", CordaOID.X509_EXTENSION_CORDA_ROLE) + val knownExtensions = setOf( + "2.5.29.15", + "2.5.29.17", + "2.5.29.18", + "2.5.29.19", + "2.5.29.37", + CordaOID.X509_EXTENSION_CORDA_ROLE + ) override fun serialize(value: X509Certificate, gen: JsonGenerator, serializers: SerializerProvider) { gen.jsonObject { @@ -208,17 +230,20 @@ object JacksonSupport { writeObjectField("issuer", value.issuerX500Principal) writeObjectField("notBefore", value.notBefore) writeObjectField("notAfter", value.notAfter) + writeObjectField("cordaCertRole", CertRole.extract(value)) writeObjectField("issuerUniqueID", value.issuerUniqueID) writeObjectField("subjectUniqueID", value.subjectUniqueID) writeObjectField("keyUsage", value.keyUsage?.asList()?.mapIndexedNotNull { i, flag -> if (flag) keyUsages[i] else null }) writeObjectField("extendedKeyUsage", value.extendedKeyUsage.map { keyPurposeIds.getOrDefault(it, it) }) jsonObject("basicConstraints") { - writeBooleanField("isCA", value.basicConstraints != -1) - writeObjectField("pathLength", value.basicConstraints.let { if (it != Int.MAX_VALUE) it else null }) + val isCa = value.basicConstraints != -1 + writeBooleanField("isCA", isCa) + if (isCa) { + writeObjectField("pathLength", value.basicConstraints.let { if (it != Int.MAX_VALUE) it else null }) + } } writeObjectField("subjectAlternativeNames", value.subjectAlternativeNames) writeObjectField("issuerAlternativeNames", value.issuerAlternativeNames) - writeObjectField("cordaCertRole", CertRole.extract(value)) writeObjectField("otherCriticalExtensions", value.criticalExtensionOIDs - knownExtensions) writeObjectField("otherNonCriticalExtensions", value.nonCriticalExtensionOIDs - knownExtensions) writeBinaryField("encoded", value.encoded) @@ -229,8 +254,12 @@ object JacksonSupport { private class X509CertificateDeserializer : JsonDeserializer() { private val certFactory = CertificateFactory.getInstance("X.509") override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): X509Certificate { - val encoded = parser.readValueAsTree()["encoded"] - return certFactory.generateCertificate(encoded.binaryValue().inputStream()) as X509Certificate + val encoded = if (parser.currentToken == JsonToken.START_OBJECT) { + parser.readValueAsTree()["encoded"].binaryValue() + } else { + parser.binaryValue + } + return certFactory.generateCertificate(encoded.inputStream()) as X509Certificate } } @@ -274,9 +303,13 @@ object JacksonSupport { @Deprecated("This is an internal class, do not use") object PartySerializer : JsonSerializer() { - override fun serialize(value: Party, generator: JsonGenerator, provider: SerializerProvider) { - // TODO Add configurable option to output this as an object which includes the owningKey - generator.writeObject(value.name) + override fun serialize(value: Party, gen: JsonGenerator, provider: SerializerProvider) { + val mapper = gen.codec as PartyObjectMapper + if (mapper.isFullParties) { + gen.writeObject(PartyAnalogue(value.name, value.owningKey)) + } else { + gen.writeObject(value.name) + } } } @@ -284,28 +317,39 @@ object JacksonSupport { object PartyDeserializer : JsonDeserializer() { override fun deserialize(parser: JsonParser, context: DeserializationContext): Party { val mapper = parser.codec as PartyObjectMapper - // The comma character is invalid in Base58, and required as a separator for X.500 names. As Corda - // X.500 names all involve at least three attributes (organisation, locality, country), they must - // include a comma. As such we can use it as a distinguisher between the two types. - return if ("," in parser.text) { - val principal = CordaX500Name.parse(parser.text) - mapper.wellKnownPartyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name $principal") + return if (parser.currentToken == JsonToken.START_OBJECT) { + val analogue = parser.readValueAs() + Party(analogue.name, analogue.owningKey) } else { - val nameMatches = mapper.partiesFromName(parser.text) - when { - nameMatches.isEmpty() -> { - val publicKey = parser.readValueAs() - mapper.partyFromKey(publicKey) - ?: throw JsonParseException(parser, "Could not find a Party with key ${publicKey.toStringShort()}") - } - nameMatches.size == 1 -> nameMatches.first() - else -> throw JsonParseException(parser, "Ambiguous name match '${parser.text}': could be any of " + - nameMatches.map { it.name }.joinToString(" ... or ... ")) + // The comma character is invalid in Base58, and required as a separator for X.500 names. As Corda + // X.500 names all involve at least three attributes (organisation, locality, country), they must + // include a comma. As such we can use it as a distinguisher between the two types. + if ("," in parser.text) { + val principal = CordaX500Name.parse(parser.text) + mapper.wellKnownPartyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name $principal") + } else { + lookupByNameSegment(mapper, parser) } } } + + private fun lookupByNameSegment(mapper: PartyObjectMapper, parser: JsonParser): Party { + val nameMatches = mapper.partiesFromName(parser.text) + return when { + nameMatches.isEmpty() -> { + val publicKey = parser.readValueAs() + mapper.partyFromKey(publicKey) + ?: throw JsonParseException(parser, "Could not find a Party with key ${publicKey.toStringShort()}") + } + nameMatches.size == 1 -> nameMatches.first() + else -> throw JsonParseException(parser, "Ambiguous name match '${parser.text}': could be any of " + + nameMatches.map { it.name }.joinToString(" ... or ... ")) + } + } } + private class PartyAnalogue(val name: CordaX500Name, val owningKey: PublicKey) + @Deprecated("This is an internal class, do not use") object CordaX500NameDeserializer : JsonDeserializer() { override fun deserialize(parser: JsonParser, context: DeserializationContext): CordaX500Name { diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt index 640dbf3a4a..5dd3fe395a 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt @@ -29,9 +29,10 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.serialization.internal.AllWhitelist import net.corda.serialization.internal.amqp.SerializerFactory import net.corda.serialization.internal.amqp.constructorForDeserialization -import net.corda.serialization.internal.amqp.createSerializerFactoryFactory +import net.corda.serialization.internal.amqp.hasCordaSerializable import net.corda.serialization.internal.amqp.propertiesForSerialization import java.security.PublicKey +import java.security.cert.CertPath class CordaModule : SimpleModule("corda-core") { override fun setupModule(context: SetupContext) { @@ -39,7 +40,7 @@ class CordaModule : SimpleModule("corda-core") { context.addBeanSerializerModifier(CordaSerializableBeanSerializerModifier()) - context.setMixInAnnotations(PartyAndCertificate::class.java, PartyAndCertificateSerializerMixin::class.java) + context.setMixInAnnotations(PartyAndCertificate::class.java, PartyAndCertificateMixin::class.java) context.setMixInAnnotations(NetworkHostAndPort::class.java, NetworkHostAndPortMixin::class.java) context.setMixInAnnotations(CordaX500Name::class.java, CordaX500NameMixin::class.java) context.setMixInAnnotations(Amount::class.java, AmountMixin::class.java) @@ -53,7 +54,7 @@ class CordaModule : SimpleModule("corda-core") { context.setMixInAnnotations(DigitalSignature.WithKey::class.java, ByteSequenceWithPropertiesMixin::class.java) context.setMixInAnnotations(DigitalSignatureWithCert::class.java, ByteSequenceWithPropertiesMixin::class.java) context.setMixInAnnotations(TransactionSignature::class.java, ByteSequenceWithPropertiesMixin::class.java) - context.setMixInAnnotations(SignedTransaction::class.java, SignedTransactionMixin2::class.java) + context.setMixInAnnotations(SignedTransaction::class.java, SignedTransactionMixin::class.java) context.setMixInAnnotations(WireTransaction::class.java, JacksonSupport.WireTransactionMixin::class.java) context.setMixInAnnotations(NodeInfo::class.java, NodeInfoMixin::class.java) } @@ -69,12 +70,15 @@ private class CordaSerializableBeanSerializerModifier : BeanSerializerModifier() override fun changeProperties(config: SerializationConfig, beanDesc: BeanDescription, beanProperties: MutableList): MutableList { - // TODO We're assuming here that Jackson gives us a superset of all the properties. Either confirm this or - // make sure the returned beanProperties are exactly the AMQP properties - if (beanDesc.beanClass.isAnnotationPresent(CordaSerializable::class.java)) { + if (hasCordaSerializable(beanDesc.beanClass)) { val ctor = constructorForDeserialization(beanDesc.beanClass) - val amqpProperties = propertiesForSerialization(ctor, beanDesc.beanClass, serializerFactory).serializationOrder - beanProperties.removeIf { bean -> amqpProperties.none { amqp -> amqp.serializer.name == bean.name } } + val amqpProperties = propertiesForSerialization(ctor, beanDesc.beanClass, serializerFactory) + .serializationOrder + .map { it.serializer.name } + beanProperties.removeIf { it.name !in amqpProperties } + (amqpProperties - beanProperties.map { it.name }).let { + check(it.isEmpty()) { "Jackson didn't provide serialisers for $it" } + } } return beanProperties } @@ -85,26 +89,31 @@ private class CordaSerializableBeanSerializerModifier : BeanSerializerModifier() private interface NetworkHostAndPortMixin private class NetworkHostAndPortDeserializer : JsonDeserializer() { - override fun deserialize(parser: JsonParser, ctxt: DeserializationContext) = NetworkHostAndPort.parse(parser.text) + override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): NetworkHostAndPort { + return NetworkHostAndPort.parse(parser.text) + } } @JsonSerialize(using = PartyAndCertificateSerializer::class) // TODO Add deserialization which follows the same lookup logic as Party -private interface PartyAndCertificateSerializerMixin +private interface PartyAndCertificateMixin private class PartyAndCertificateSerializer : JsonSerializer() { override fun serialize(value: PartyAndCertificate, gen: JsonGenerator, serializers: SerializerProvider) { - gen.jsonObject { - writeObjectField("name", value.name) - writeObjectField("owningKey", value.owningKey) - // TODO Add configurable option to output the certPath + val mapper = gen.codec as JacksonSupport.PartyObjectMapper + if (mapper.isFullParties) { + gen.writeObject(PartyAndCertificateWrapper(value.name, value.certPath)) + } else { + gen.writeObject(value.party) } } } +private class PartyAndCertificateWrapper(val name: CordaX500Name, val certPath: CertPath) + @JsonSerialize(using = SignedTransactionSerializer::class) @JsonDeserialize(using = SignedTransactionDeserializer::class) -private interface SignedTransactionMixin2 +private interface SignedTransactionMixin private class SignedTransactionSerializer : JsonSerializer() { override fun serialize(value: SignedTransaction, gen: JsonGenerator, serializers: SerializerProvider) { diff --git a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt index 5847e52c10..4283523a33 100644 --- a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt +++ b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt @@ -14,10 +14,7 @@ import net.corda.core.contracts.Amount import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.* import net.corda.core.crypto.CompositeKey -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.AnonymousParty -import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.Party +import net.corda.core.identity.* import net.corda.core.internal.DigitalSignatureWithCert import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub @@ -247,10 +244,20 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: assertThat(json.textValue()).isEqualTo(MINI_CORP.name.toString()) } + @Test + fun `Party serialization with isFullParty = true`() { + partyObjectMapper.isFullParties = true + val json = mapper.valueToTree(MINI_CORP.party) + val (name, owningKey) = json.assertHasOnlyFields("name", "owningKey") + assertThat(name.valueAs(mapper)).isEqualTo(MINI_CORP.name) + assertThat(owningKey.valueAs(mapper)).isEqualTo(MINI_CORP.publicKey) + } + @Test fun `Party deserialization on full name`() { fun convertToParty() = mapper.convertValue(TextNode(MINI_CORP.name.toString())) + // Check that it fails if it can't find the party assertThatThrownBy { convertToParty() } partyObjectMapper.identities += MINI_CORP.party @@ -261,6 +268,7 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: fun `Party deserialization on part of name`() { fun convertToParty() = mapper.convertValue(TextNode(MINI_CORP.name.organisation)) + // Check that it fails if it can't find the party assertThatThrownBy { convertToParty() } partyObjectMapper.identities += MINI_CORP.party @@ -271,12 +279,24 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: fun `Party deserialization on public key`() { fun convertToParty() = mapper.convertValue(TextNode(MINI_CORP.publicKey.toBase58String())) + // Check that it fails if it can't find the party assertThatThrownBy { convertToParty() } partyObjectMapper.identities += MINI_CORP.party assertThat(convertToParty()).isEqualTo(MINI_CORP.party) } + @Test + fun `Party deserialization on name and key`() { + val party = mapper.convertValue(mapOf( + "name" to MINI_CORP.name, + "owningKey" to MINI_CORP.publicKey + )) + // Party.equals is only defined on the public key so we must check the name as well + assertThat(party.name).isEqualTo(MINI_CORP.name) + assertThat(party.owningKey).isEqualTo(MINI_CORP.publicKey) + } + @Test fun PublicKey() { val json = mapper.valueToTree(MINI_CORP.publicKey) @@ -316,15 +336,31 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: } @Test - fun `PartyAndCertificate serialisation`() { - val json = mapper.valueToTree(MINI_CORP.identity) - val (name, owningKey) = json.assertHasOnlyFields("name", "owningKey") - assertThat(name.valueAs(mapper)).isEqualTo(MINI_CORP.name) - assertThat(owningKey.valueAs(mapper)).isEqualTo(MINI_CORP.publicKey) + fun `PartyAndCertificate serialization`() { + val json = mapper.valueToTree(MINI_CORP.identity) + assertThat(json.textValue()).isEqualTo(MINI_CORP.name.toString()) } @Test - fun `NodeInfo serialisation`() { + fun `PartyAndCertificate serialization with isFullParty = true`() { + partyObjectMapper.isFullParties = true + val json = mapper.valueToTree(MINI_CORP.identity) + println(mapper.writeValueAsString(json)) + val (name, certPath) = json.assertHasOnlyFields("name", "certPath") + assertThat(name.valueAs(mapper)).isEqualTo(MINI_CORP.name) + assertThat(certPath.valueAs(mapper)).isEqualTo(MINI_CORP.identity.certPath) + } + + @Test + fun `PartyAndCertificate deserialization on cert path`() { + val certPathJson = mapper.valueToTree(MINI_CORP.identity.certPath) + val partyAndCert = mapper.convertValue(mapOf("certPath" to certPathJson)) + // PartyAndCertificate.equals is defined on the Party so we must check the certPath directly + assertThat(partyAndCert.certPath).isEqualTo(MINI_CORP.identity.certPath) + } + + @Test + fun `NodeInfo serialization`() { val (nodeInfo) = createNodeInfoAndSigned(ALICE_NAME) val json = mapper.valueToTree(nodeInfo) val (addresses, legalIdentitiesAndCerts, platformVersion, serial) = json.assertHasOnlyFields( @@ -339,14 +375,14 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: } legalIdentitiesAndCerts.run { assertThat(this).hasSize(1) - assertThat(this[0]["name"].valueAs(mapper)).isEqualTo(ALICE_NAME) + assertThat(this[0].valueAs(mapper)).isEqualTo(ALICE_NAME) } assertThat(platformVersion.intValue()).isEqualTo(nodeInfo.platformVersion) assertThat(serial.longValue()).isEqualTo(nodeInfo.serial) } @Test - fun `NodeInfo deserialisation on name`() { + fun `NodeInfo deserialization on name`() { val (nodeInfo) = createNodeInfoAndSigned(ALICE_NAME) fun convertToNodeInfo() = mapper.convertValue(TextNode(ALICE_NAME.toString())) @@ -359,7 +395,7 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: } @Test - fun `NodeInfo deserialisation on public key`() { + fun `NodeInfo deserialization on public key`() { val (nodeInfo) = createNodeInfoAndSigned(ALICE_NAME) fun convertToNodeInfo() = mapper.convertValue(TextNode(nodeInfo.legalIdentities[0].owningKey.toBase58String())) @@ -386,7 +422,7 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: } @Test - fun X509Certificate() { + fun `X509Certificate serialization`() { val cert: X509Certificate = MINI_CORP.identity.certificate val json = mapper.valueToTree(cert) println(mapper.writeValueAsString(json)) @@ -397,7 +433,13 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: assertThat(json["notAfter"].valueAs(mapper)).isEqualTo(cert.notAfter) assertThat(json["notBefore"].valueAs(mapper)).isEqualTo(cert.notBefore) assertThat(json["encoded"].binaryValue()).isEqualTo(cert.encoded) - assertThat(mapper.convertValue(json).encoded).isEqualTo(cert.encoded) + } + + @Test + fun `X509Certificate deserialization`() { + val cert: X509Certificate = MINI_CORP.identity.certificate + assertThat(mapper.convertValue(mapOf("encoded" to cert.encoded))).isEqualTo(cert) + assertThat(mapper.convertValue(BinaryNode(cert.encoded))).isEqualTo(cert) } @Test @@ -448,6 +490,7 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: } private class TestPartyObjectMapper : JacksonSupport.PartyObjectMapper { + override var isFullParties: Boolean = false val identities = ArrayList() val nodes = ArrayList() override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? { diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index 5f4472b884..43338d4706 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -80,6 +80,17 @@ import kotlin.reflect.KClass import kotlin.reflect.full.createInstance val Throwable.rootCause: Throwable get() = cause?.rootCause ?: this +val Throwable.rootMessage: String? get() { + var message = this.message + var throwable = cause + while (throwable != null) { + if (throwable.message != null) { + message = throwable.message + } + throwable = throwable.cause + } + return message +} infix fun Temporal.until(endExclusive: Temporal): Duration = Duration.between(this, endExclusive) diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt index c9b8cc686c..02447e9a2b 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt @@ -209,7 +209,8 @@ object SerializationDefaults { /** * Convenience extension method for deserializing a ByteSequence, utilising the defaults. */ -inline fun ByteSequence.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): T { +inline fun ByteSequence.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, + context: SerializationContext = serializationFactory.defaultContext): T { return serializationFactory.deserialize(this, T::class.java, context) } @@ -218,31 +219,40 @@ inline fun ByteSequence.deserialize(serializationFactory: Seri * It might be helpful to know [SerializationContext] to use the same encoding in the reply. */ inline fun ByteSequence.deserializeWithCompatibleContext(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, - context: SerializationContext = serializationFactory.defaultContext): ObjectWithCompatibleContext { + context: SerializationContext = serializationFactory.defaultContext): ObjectWithCompatibleContext { return serializationFactory.deserializeWithCompatibleContext(this, T::class.java, context) } /** * Convenience extension method for deserializing SerializedBytes with type matching, utilising the defaults. */ -inline fun SerializedBytes.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): T { +inline fun SerializedBytes.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, + context: SerializationContext = serializationFactory.defaultContext): T { return serializationFactory.deserialize(this, T::class.java, context) } /** * Convenience extension method for deserializing a ByteArray, utilising the defaults. */ -inline fun ByteArray.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): T = this.sequence().deserialize(serializationFactory, context) +inline fun ByteArray.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, + context: SerializationContext = serializationFactory.defaultContext): T { + require(isNotEmpty()) { "Empty bytes" } + return this.sequence().deserialize(serializationFactory, context) +} /** * Convenience extension method for deserializing a JDBC Blob, utilising the defaults. */ -inline fun Blob.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): T = this.getBytes(1, this.length().toInt()).deserialize(serializationFactory, context) +inline fun Blob.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, + context: SerializationContext = serializationFactory.defaultContext): T { + return this.getBytes(1, this.length().toInt()).deserialize(serializationFactory, context) +} /** * Convenience extension method for serializing an object of type T, utilising the defaults. */ -fun T.serialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): SerializedBytes { +fun T.serialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, + context: SerializationContext = serializationFactory.defaultContext): SerializedBytes { return serializationFactory.serialize(this, context) } diff --git a/docs/source/blob-inspector.rst b/docs/source/blob-inspector.rst new file mode 100644 index 0000000000..ba301eef0a --- /dev/null +++ b/docs/source/blob-inspector.rst @@ -0,0 +1,63 @@ +Blob Inspector +============== + +There are many benefits to having a custom binary serialisation format (see :doc:`serialization` for details) but one +disadvantage is the inability to view the contents in a human-friendly manner. The blob inspector tool alleviates this issue +by allowing the contents of a binary blob file (or URL end-point) to be output in either YAML or JSON. It uses +``JacksonSupport`` to do this (see :doc:`json`). + +The latest version of the tool can be downloaded from `here `_. + +To run simply pass in the file or URL as the first parameter: + +``java -jar blob-inspector.jar `` + +Use the ``--help`` flag for a full list of command line options. + +``SerializedBytes` +~~~~~~~~~~~~~~~~~~ + +One thing to note is that the binary blob may contain embedded ``SerializedBytes`` objects. Rather than printing these +out as a Base64 string, the blob inspector will first materialise them into Java objects and then output those. You will +see this when dealing with classes such as ``SignedData`` or other structures that attach a signature, such as the +``nodeInfo-*`` files or the ``network-parameters`` file in the node's directory. For example, the output of a node-info +file may look like: + +.. container:: codeset + + .. sourcecode:: yaml + + net.corda.nodeapi.internal.SignedNodeInfo + --- + raw: + class: "net.corda.core.node.NodeInfo" + deserialized: + addresses: + - "localhost:10011" + legalIdentitiesAndCerts: + - "O=BankOfCorda, L=New York, C=US" + platformVersion: 4 + serial: 1527074180971 + signatures: + - !!binary | + dmoAnnzcv0MzRN+3ZSCDcCJIAbXnoYy5mFWB3Nijndzu/dzIoYdIawINXbNSY/5z2XloDK01vZRV + TreFZCbZAg== + + .. sourcecode:: json + + net.corda.nodeapi.internal.SignedNodeInfo + { + "raw" : { + "class" : "net.corda.core.node.NodeInfo", + "deserialized" : { + "addresses" : [ "localhost:10011" ], + "legalIdentitiesAndCerts" : [ "O=BankOfCorda, L=New York, C=US" ], + "platformVersion" : 4, + "serial" : 1527074180971 + } + }, + "signatures" : [ "dmoAnnzcv0MzRN+3ZSCDcCJIAbXnoYy5mFWB3Nijndzu/dzIoYdIawINXbNSY/5z2XloDK01vZRVTreFZCbZAg==" ] + } + +Notice the file is actually a serialised ``SignedNodeInfo`` object, which has a ``raw`` property of type ``SerializedBytes``. +This property is materialised into a ``NodeInfo`` and is output under the ``deserialized`` field. diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index b79be24fa3..4d2931eb76 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -34,12 +34,19 @@ Unreleased * ``Party`` objects can be deserialised by looking up their public key, in addition to their name * ``NodeInfo`` objects are serialised as an object and can be looked up using the same mechanism as ``Party`` * ``NetworkHostAndPort`` serialised according to its ``toString()`` - * ``PartyAndCertificate`` is serialised as an object containing the name and owning key - * ``SerializedBytes`` is serialised by converting the bytes into the object it represents, which is then serialised into - a JSON/YAML object - * ``CertPath`` and ``X509Certificate`` are serialised as objects and can be deserialised back + * ``PartyAndCertificate`` is serialised as the name + * ``SerializedBytes`` is serialised by materialising the bytes into the object it represents, and then serialising that + object into YAML/JSON + * ``X509Certificate`` is serialised as an object with key fields such as ``issuer``, ``publicKey``, ``serialNumber``, etc. + The encoded bytes are also serialised into the ``encoded`` field. This can be used to deserialise an ``X509Certificate`` + back. + * ``CertPath`` objects are serialised as a list of ``X509Certificate`` objects. * ``SignedTransaction`` is serialised into its ``txBits`` and ``signatures`` and can be deserialised back +* ``fullParties`` boolean parameter added to ``JacksonSupport.createDefaultMapper`` and ``createNonRpcMapper``. If ``true`` + then ``Party`` objects are serialised as JSON objects with the ``name`` and ``owningKey`` fields. For ``PartyAndCertificate`` + the ``certPath`` is serialised. + * Several members of ``JacksonSupport`` have been deprecated to highlight that they are internal and not to be used. * The Vault Criteria API has been extended to take a more precise specification of which class contains a field. This diff --git a/docs/source/tools-index.rst b/docs/source/tools-index.rst index 55b398dd35..c9bcafc44b 100644 --- a/docs/source/tools-index.rst +++ b/docs/source/tools-index.rst @@ -4,6 +4,7 @@ Tools .. toctree:: :maxdepth: 1 + blob-inspector network-simulator demobench node-explorer diff --git a/experimental/blobinspector/build.gradle b/experimental/blobinspector/build.gradle deleted file mode 100644 index 2862ff6fae..0000000000 --- a/experimental/blobinspector/build.gradle +++ /dev/null @@ -1,52 +0,0 @@ -apply plugin: 'java' -apply plugin: 'kotlin' -apply plugin: 'application' - -mainClassName = 'net.corda.blobinspector.MainKt' - -dependencies { - compile project(':core') - compile project(':node-api') - - compile "commons-cli:commons-cli:$commons_cli_version" - - testCompile project(':test-utils') - - testCompile "junit:junit:$junit_version" -} - -/** - * To run from within gradle use - * - * ./gradlew -PrunArgs=" " :experimental:blobinspector:run - * - * For example, to parse a file from the command line and print out the deserialized properties - * - * ./gradlew -PrunArgs="-f -d" :experimental:blobinspector:run - * - * at the command line. - */ -run { - if (project.hasProperty('runArgs')) { - args = [ project.findProperty('runArgs').toString().split(" ") ].flatten() - } - - if (System.properties.getProperty('consoleLogLevel') != null) { - logging.captureStandardOutput(LogLevel.valueOf(System.properties.getProperty('consoleLogLevel'))) - logging.captureStandardError(LogLevel.valueOf(System.properties.getProperty('consoleLogLevel'))) - systemProperty "consoleLogLevel", System.properties.getProperty('consoleLogLevel') - } -} - -/** - * Build a executable jar - */ -jar { - baseName 'blobinspector' - manifest { - attributes( - 'Automatic-Module-Name': 'net.corda.experimental.blobinspector', - 'Main-Class': 'net.corda.blobinspector.MainKt' - ) - } -} diff --git a/experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt b/experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt deleted file mode 100644 index 84de454358..0000000000 --- a/experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt +++ /dev/null @@ -1,405 +0,0 @@ -package net.corda.blobinspector - -import net.corda.core.crypto.SecureHash -import net.corda.core.serialization.EncodingWhitelist -import net.corda.core.serialization.SerializationEncoding -import net.corda.core.utilities.ByteSequence -import net.corda.serialization.internal.SerializationFactoryImpl -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 net.corda.serialization.internal.amqp.amqpMagic -import org.apache.qpid.proton.amqp.Binary -import org.apache.qpid.proton.amqp.DescribedType -import org.apache.qpid.proton.amqp.Symbol - -/** - * Print a string to the console only if the verbose config option is set. - */ -fun String.debug(config: Config) { - if (config.verbose) { - println(this) - } -} - -/** - * - */ -interface Stringify { - fun stringify(sb: IndentingStringBuilder) -} - -/** - * Makes classnames easier to read by stripping off the package names from the class and separating nested - * classes - * - * For example: - * - * net.corda.blobinspector.Class1 - * Class1 - * - * net.corda.blobinspector.Class1 - * Class1 - * - * net.corda.blobinspector.Class1> - * Class1 > - * - * net.corda.blobinspector.Class1> - * Class1 :: C > - */ -fun String.simplifyClass(): String { - - return if (this.endsWith('>')) { - val templateStart = this.indexOf('<') - val clazz = (this.substring(0, templateStart)) - val params = this.substring(templateStart+1, this.length-1).split(',').joinToString { it.simplifyClass() } - - "${clazz.simplifyClass()} <$params>" - } - else { - substring(this.lastIndexOf('.') + 1).replace("$", " :: ") - } -} - -/** - * Represents the deserialized form of the property of an Object - * - * @param name - * @param type - */ -abstract class Property( - val name: String, - val type: String) : Stringify - -/** - * Derived class of [Property], represents properties of an object that are non compelex, such - * as any POD type or String - */ -class PrimProperty( - name: String, - type: String, - private val value: String) : Property(name, type) { - override fun toString(): String = "$name : $type : $value" - - override fun stringify(sb: IndentingStringBuilder) { - sb.appendln("$name : $type : $value") - } -} - -/** - * Derived class of [Property] that represents a binary blob. Specifically useful because printing - * a stream of bytes onto the screen isn't very use friendly - */ -class BinaryProperty( - name: String, - type: String, - val value: ByteArray) : Property(name, type) { - override fun toString(): String = "$name : $type : <<>>" - - override fun stringify(sb: IndentingStringBuilder) { - sb.appendln("$name : $type : <<>>") - } -} - -/** - * Derived class of [Property] that represent a list property. List could be either PoD types or - * composite types. - */ -class ListProperty( - name: String, - type: String, - private val values: MutableList = mutableListOf()) : Property(name, type) { - override fun stringify(sb: IndentingStringBuilder) { - sb.apply { - when { - values.isEmpty() -> appendln("$name : $type : [ << EMPTY LIST >> ]") - values.first() is Stringify -> { - appendln("$name : $type : [") - values.forEach { - (it as Stringify).stringify(this) - } - appendln("]") - } - else -> { - appendln("$name : $type : [") - values.forEach { - appendln(it.toString()) - } - appendln("]") - } - } - } - } -} - -class MapProperty( - name: String, - type: String, - private val map: MutableMap<*, *> -) : Property(name, type) { - override fun stringify(sb: IndentingStringBuilder) { - if (map.isEmpty()) { - sb.appendln("$name : $type : { << EMPTY MAP >> }") - return - } - - // TODO this will not produce pretty output - sb.apply { - appendln("$name : $type : {") - map.forEach { - try { - (it.key as Stringify).stringify(this) - } catch (e: ClassCastException) { - append (it.key.toString() + " : ") - } - try { - (it.value as Stringify).stringify(this) - } catch (e: ClassCastException) { - appendln("\"${it.value.toString()}\"") - } - } - appendln("}") - } - } -} - -/** - * Derived class of [Property] that represents class properties that are themselves instances of - * some complex type. - */ -class InstanceProperty( - name: String, - type: String, - val value: Instance) : Property(name, type) { - override fun stringify(sb: IndentingStringBuilder) { - sb.append("$name : ") - value.stringify(sb) - } -} - -/** - * Represents an instance of a composite type. - */ -class Instance( - val name: String, - val type: String, - val fields: MutableList = mutableListOf()) : Stringify { - override fun stringify(sb: IndentingStringBuilder) { - sb.apply { - appendln("${name.simplifyClass()} : {") - fields.forEach { - it.stringify(this) - } - appendln("}") - } - } -} - -/** - * - */ -fun inspectComposite( - config: Config, - typeMap: Map, - obj: DescribedType): Instance { - if (obj.described !is List<*>) throw MalformedBlob("") - - val name = (typeMap[obj.descriptor] as CompositeType).name - "composite: $name".debug(config) - - val inst = Instance( - typeMap[obj.descriptor]?.name ?: "", - typeMap[obj.descriptor]?.label ?: "") - - (typeMap[obj.descriptor] as CompositeType).fields.zip(obj.described as List<*>).forEach { - " field: ${it.first.name}".debug(config) - inst.fields.add( - if (it.second is DescribedType) { - " - is described".debug(config) - val d = inspectDescribed(config, typeMap, it.second as DescribedType) - - when (d) { - is Instance -> - InstanceProperty( - it.first.name, - it.first.type, - d) - is List<*> -> { - " - List".debug(config) - ListProperty( - it.first.name, - it.first.type, - d as MutableList) - } - is Map<*, *> -> { - MapProperty( - it.first.name, - it.first.type, - d as MutableMap<*, *>) - } - else -> { - " skip it".debug(config) - return@forEach - } - } - - } else { - " - is prim".debug(config) - when (it.first.type) { - // Note, as in the case of SHA256 we can treat particular binary types - // as different properties with a little coercion - "binary" -> { - if (name == "net.corda.core.crypto.SecureHash\$SHA256") { - PrimProperty( - it.first.name, - it.first.type, - SecureHash.SHA256((it.second as Binary).array).toString()) - } else { - BinaryProperty(it.first.name, it.first.type, (it.second as Binary).array) - } - } - else -> PrimProperty(it.first.name, it.first.type, it.second.toString()) - } - }) - } - - return inst -} - -fun inspectRestricted( - config: Config, - typeMap: Map, - obj: DescribedType): Any { - return when ((typeMap[obj.descriptor] as RestrictedType).source) { - "list" -> inspectRestrictedList(config, typeMap, obj) - "map" -> inspectRestrictedMap(config, typeMap, obj) - else -> throw NotImplementedError() - } -} - - -fun inspectRestrictedList( - config: Config, - typeMap: Map, - obj: DescribedType -) : List { - if (obj.described !is List<*>) throw MalformedBlob("") - - return mutableListOf().apply { - (obj.described as List<*>).forEach { - when (it) { - is DescribedType -> add(inspectDescribed(config, typeMap, it)) - is RestrictedType -> add(inspectRestricted(config, typeMap, it)) - else -> add (it.toString()) - } - } - } -} - -fun inspectRestrictedMap( - config: Config, - typeMap: Map, - obj: DescribedType -) : Map { - if (obj.described !is Map<*,*>) throw MalformedBlob("") - - return mutableMapOf().apply { - (obj.described as Map<*, *>).forEach { - val key = when (it.key) { - is DescribedType -> inspectDescribed(config, typeMap, it.key as DescribedType) - is RestrictedType -> inspectRestricted(config, typeMap, it.key as RestrictedType) - else -> it.key.toString() - } - - val value = when (it.value) { - is DescribedType -> inspectDescribed(config, typeMap, it.value as DescribedType) - is RestrictedType -> inspectRestricted(config, typeMap, it.value as RestrictedType) - else -> it.value.toString() - } - - this[key] = value - } - } -} - - -/** - * Every element of the blob stream will be a ProtonJ [DescribedType]. When inspecting the blob stream - * the two custom Corda types we're interested in are [CompositeType]'s, representing the instance of - * some object (class), and [RestrictedType]'s, representing containers and enumerations. - * - * @param config The configuration object that controls the behaviour of the BlobInspector - * @param typeMap - * @param obj - */ -fun inspectDescribed( - config: Config, - typeMap: Map, - obj: DescribedType): Any { - "${obj.descriptor} in typeMap? = ${obj.descriptor in typeMap}".debug(config) - - return when (typeMap[obj.descriptor]) { - is CompositeType -> { - "* It's composite".debug(config) - inspectComposite(config, typeMap, obj) - } - is RestrictedType -> { - "* It's restricted".debug(config) - inspectRestricted(config, typeMap, obj) - } - else -> { - "${typeMap[obj.descriptor]?.name} is neither Composite or Restricted".debug(config) - } - } - -} - -internal object NullEncodingWhitelist : EncodingWhitelist { - override fun acceptEncoding(encoding: SerializationEncoding) = false -} - -// TODO : Refactor to generically poerate on arbitrary blobs, not a single workflow -fun inspectBlob(config: Config, blob: ByteArray) { - val bytes = ByteSequence.of(blob) - - val headerSize = SerializationFactoryImpl.magicSize - - // TODO written to only understand one version, when we support multiple this will need to change - val headers = listOf(ByteSequence.of(amqpMagic.bytes)) - - val blobHeader = bytes.take(headerSize) - - if (blobHeader !in headers) { - throw MalformedBlob("Blob is not a Corda AMQP serialised object graph") - } - - - val e = DeserializationInput.getEnvelope(bytes, NullEncodingWhitelist) - - if (config.schema) { - println(e.schema) - } - - if (config.transforms) { - println(e.transformsSchema) - } - - val typeMap = e.schema.types.associateBy({ it.descriptor.name }, { it }) - - if (config.data) { - val inspected = inspectDescribed(config, typeMap, e.obj as DescribedType) - - println("\n${IndentingStringBuilder().apply { (inspected as Instance).stringify(this) }}") - - (inspected as Instance).fields.find { - it.type.startsWith("net.corda.core.serialization.SerializedBytes<") - }?.let { - "Found field of SerializedBytes".debug(config) - (it as InstanceProperty).value.fields.find { it.name == "bytes" }?.let { raw -> - inspectBlob(config, (raw as BinaryProperty).value) - } - } - } -} - diff --git a/experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobLoader.kt b/experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobLoader.kt deleted file mode 100644 index c831665036..0000000000 --- a/experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobLoader.kt +++ /dev/null @@ -1,40 +0,0 @@ -package net.corda.blobinspector - -import java.io.File -import java.net.URL - -/** - * - */ -class FileBlobHandler(config_: Config) : BlobHandler(config_) { - private val path = File(URL((config_ as FileConfig).file).toURI()) - - override fun getBytes(): ByteArray { - return path.readBytes() - } -} - -/** - * - */ -class InMemoryBlobHandler(config_: Config) : BlobHandler(config_) { - private val localBytes = (config_ as InMemoryConfig).blob?.bytes ?: kotlin.ByteArray(0) - override fun getBytes(): ByteArray = localBytes -} - -/** - * - */ -abstract class BlobHandler(val config: Config) { - companion object { - fun make(config: Config): BlobHandler { - return when (config.mode) { - Mode.file -> FileBlobHandler(config) - Mode.inMem -> InMemoryBlobHandler(config) - } - } - } - - abstract fun getBytes(): ByteArray -} - diff --git a/experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/Config.kt b/experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/Config.kt deleted file mode 100644 index 376331ec2b..0000000000 --- a/experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/Config.kt +++ /dev/null @@ -1,137 +0,0 @@ -package net.corda.blobinspector - -import org.apache.commons.cli.CommandLine -import net.corda.core.serialization.SerializedBytes -import org.apache.commons.cli.Option -import org.apache.commons.cli.Options - -/** - * Enumeration of the modes in which the blob inspector can be run. - * - * @property make lambda function that takes no parameters and returns a specific instance of the configuration - * object for that mode. - * - * @property options A lambda function that takes no parameters and returns an [Options] instance that define - * the command line flags related to this mode. For example ``file`` mode would have an option to pass in - * the name of the file to read. - * - */ -enum class Mode( - val make : () -> Config, - val options : (Options) -> Unit -) { - file( - { - FileConfig(Mode.file) - }, - { o -> - o.apply{ - addOption( - Option ("f", "file", true, "path to file").apply { - isRequired = true - } - ) - } - } - ), - inMem( - { - InMemoryConfig(Mode.inMem) - }, - { - // The in memory only mode has no specific option assocaited with it as it's intended for - // testing purposes only within the unit test framework and not use on the command line - } - ) -} - -/** - * Configuration data class for the Blob Inspector. - * - * @property mode - */ -abstract class Config (val mode: Mode) { - var schema: Boolean = false - var transforms: Boolean = false - var data: Boolean = false - var verbose: Boolean = false - - abstract fun populateSpecific(cmdLine: CommandLine) - abstract fun withVerbose() : Config - - fun populate(cmdLine: CommandLine) { - schema = cmdLine.hasOption('s') - transforms = cmdLine.hasOption('t') - data = cmdLine.hasOption('d') - verbose = cmdLine.hasOption('v') - - populateSpecific(cmdLine) - } - - fun options() = Options().apply { - // install generic options - addOption(Option("s", "schema", false, "print the blob's schema").apply { - isRequired = false - }) - - addOption(Option("t", "transforms", false, "print the blob's transforms schema").apply { - isRequired = false - }) - - addOption(Option("d", "data", false, "Display the serialised data").apply { - isRequired = false - }) - - addOption(Option("v", "verbose", false, "Enable debug output").apply { - isRequired = false - }) - - // install the mode specific options - mode.options(this) - } -} - - -/** - * Configuration object when running in "File" mode, i.e. the object has been specified at - * the command line - */ -class FileConfig ( - mode: Mode -) : Config(mode) { - - var file: String = "unset" - - override fun populateSpecific(cmdLine : CommandLine) { - file = cmdLine.getParsedOptionValue("f") as String - } - - override fun withVerbose() : FileConfig { - return FileConfig(mode).apply { - this.schema = schema - this.transforms = transforms - this.data = data - this.verbose = true - } - } -} - - -/** - * Placeholder config objet used when running unit tests and the inspected blob is being fed in - * via some mechanism directly. Normally this will be the direct serialisation of an object in a unit - * test and then dumping that blob into the inspector for visual comparison of the output - */ -class InMemoryConfig ( - mode: Mode -) : Config(mode) { - var blob: SerializedBytes<*>? = null - - override fun populateSpecific(cmdLine: CommandLine) { - throw UnsupportedOperationException("In memory config is for testing only and cannot set specific flags") - } - - override fun withVerbose(): Config { - throw UnsupportedOperationException("In memory config is for testing headlessly, cannot be verbose") - } -} diff --git a/experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/Errors.kt b/experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/Errors.kt deleted file mode 100644 index 888ef1e302..0000000000 --- a/experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/Errors.kt +++ /dev/null @@ -1,3 +0,0 @@ -package net.corda.blobinspector - -class MalformedBlob(msg: String) : Exception(msg) diff --git a/experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/IndentingStringBuilder.kt b/experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/IndentingStringBuilder.kt deleted file mode 100644 index 1ec7fe6557..0000000000 --- a/experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/IndentingStringBuilder.kt +++ /dev/null @@ -1,44 +0,0 @@ -package net.corda.blobinspector - -/** - * Wrapper around a [StringBuilder] that automates the indenting of lines as they're appended to facilitate - * pretty printing of deserialized blobs. - * - * @property sb The wrapped [StringBuilder] - * @property indenting Boolean flag that indicates weather we need to pad the start of whatever text - * currently being added to the string. - * @property indent How deeply the next line should be offset from the first column - */ -class IndentingStringBuilder(s: String = "", private val offset: Int = 4) { - private val sb = StringBuilder(s) - private var indenting = true - private var indent = 0 - - private fun wrap(ln: String, appender: (String) -> Unit) { - if ((ln.endsWith("}") || ln.endsWith("]")) && indent > 0 && ln.length == 1) { - indent -= offset - } - - appender(ln) - - if (ln.endsWith("{") || ln.endsWith("[")) { - indent += offset - } - } - - fun appendln(ln: String) { - wrap(ln) { s -> sb.appendln("${"".padStart(if (indenting) indent else 0, ' ')}$s") } - - indenting = true - } - - fun append(ln: String) { - indenting = false - - wrap(ln) { s -> sb.append("${"".padStart(indent, ' ')}$s") } - } - - override fun toString(): String { - return sb.toString() - } -} \ No newline at end of file diff --git a/experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/Main.kt b/experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/Main.kt deleted file mode 100644 index 9abe288afa..0000000000 --- a/experimental/blobinspector/src/main/kotlin/net/corda/blobinspector/Main.kt +++ /dev/null @@ -1,81 +0,0 @@ -package net.corda.blobinspector - -import org.apache.commons.cli.* -import java.lang.IllegalArgumentException - -/** - * Mode isn't a required property as we default it to [Mode.file] - */ -private fun modeOption() = Option("m", "mode", true, "mode, file is the default").apply { - isRequired = false -} - -/** - * - * Parse the command line arguments looking for the main mode into which the application is - * being put. Note, this defaults to [Mode.file] if not set meaning we will look for a file path - * being passed as a parameter and parse that file. - * - * @param args reflects the command line arguments - * - * @return An instantiated but unpopulated [Config] object instance suitable for the mode into - * which we've been placed. This Config object should be populated via [loadModeSpecificOptions] - */ -fun getMode(args: Array): Config { - // For now we only care what mode we're being put in, we can build the rest of the args and parse them - // later - val options = Options().apply { - addOption(modeOption()) - } - - val cmd = try { - DefaultParser().parse(options, args, true) - } catch (e: org.apache.commons.cli.ParseException) { - println(e) - HelpFormatter().printHelp("blobinspector", options) - throw IllegalArgumentException("OH NO!!!") - } - - return try { - Mode.valueOf(cmd.getParsedOptionValue("m") as? String ?: "file") - } catch (e: IllegalArgumentException) { - Mode.file - }.make() -} - -/** - * - * @param config an instance of a [Config] specialisation suitable for the mode into which - * the application has been put. - * @param args The command line arguments - */ -fun loadModeSpecificOptions(config: Config, args: Array) { - config.apply { - // load that modes specific command line switches, needs to include the mode option - val modeSpecificOptions = config.options().apply { - addOption(modeOption()) - } - - populate(try { - DefaultParser().parse(modeSpecificOptions, args, false) - } catch (e: org.apache.commons.cli.ParseException) { - println("Error: ${e.message}") - HelpFormatter().printHelp("blobinspector", modeSpecificOptions) - System.exit(1) - return - }) - } -} - -/** - * Executable entry point - */ -fun main(args: Array) { - println("<<< WARNING: this tool is experimental and under active development >>>") - getMode(args).let { mode -> - loadModeSpecificOptions(mode, args) - BlobHandler.make(mode) - }.apply { - inspectBlob(config, getBytes()) - } -} diff --git a/experimental/blobinspector/src/test/kotlin/net/corda/blobinspector/FileParseTests.kt b/experimental/blobinspector/src/test/kotlin/net/corda/blobinspector/FileParseTests.kt deleted file mode 100644 index c0a87a667e..0000000000 --- a/experimental/blobinspector/src/test/kotlin/net/corda/blobinspector/FileParseTests.kt +++ /dev/null @@ -1,87 +0,0 @@ -package net.corda.blobinspector - -import java.net.URI - -import org.junit.Test -import net.corda.testing.common.internal.ProjectStructure.projectRootDir - -class FileParseTests { - @Suppress("UNUSED") - var localPath: URI = projectRootDir.toUri().resolve( - "tools/blobinspector/src/test/resources/net/corda/blobinspector") - - fun setupArgsWithFile(path: String) = Array(5) { - when (it) { - 0 -> "-m" - 1 -> "file" - 2 -> "-f" - 3 -> path - 4 -> "-d" - else -> "error" - } - } - - private val filesToTest = listOf( - "FileParseTests.1Int", - "FileParseTests.2Int", - "FileParseTests.3Int", - "FileParseTests.1String", - "FileParseTests.1Composite", - "FileParseTests.2Composite", - "FileParseTests.IntList", - "FileParseTests.StringList", - "FileParseTests.MapIntString", - "FileParseTests.MapIntClass" - ) - - fun testFile(file: String) { - val path = FileParseTests::class.java.getResource(file) - val args = setupArgsWithFile(path.toString()) - - val handler = getMode(args).let { mode -> - loadModeSpecificOptions(mode, args) - BlobHandler.make(mode) - } - - inspectBlob(handler.config, handler.getBytes()) - } - - @Test - fun simpleFiles() { - filesToTest.forEach { testFile(it) } - } - - @Test - fun specificTest() { - testFile(filesToTest[4]) - testFile(filesToTest[5]) - testFile(filesToTest[6]) - } - - @Test - fun networkParams() { - val file = "networkParams" - val path = FileParseTests::class.java.getResource(file) - val verbose = false - - val args = verbose.let { - if (it) - Array(4) { - when (it) { 0 -> "-f"; 1 -> path.toString(); 2 -> "-d"; 3 -> "-vs"; else -> "error" - } - } - else - Array(3) { - when (it) { 0 -> "-f"; 1 -> path.toString(); 2 -> "-d"; else -> "error" - } - } - } - - val handler = getMode(args).let { mode -> - loadModeSpecificOptions(mode, args) - BlobHandler.make(mode) - } - - inspectBlob(handler.config, handler.getBytes()) - } -} diff --git a/experimental/blobinspector/src/test/kotlin/net/corda/blobinspector/InMemoryTests.kt b/experimental/blobinspector/src/test/kotlin/net/corda/blobinspector/InMemoryTests.kt deleted file mode 100644 index 4b94bf2eea..0000000000 --- a/experimental/blobinspector/src/test/kotlin/net/corda/blobinspector/InMemoryTests.kt +++ /dev/null @@ -1,91 +0,0 @@ -package net.corda.blobinspector - -import net.corda.core.serialization.SerializedBytes -import net.corda.serialization.internal.AllWhitelist -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.AMQP_P2P_CONTEXT -import org.junit.Test - - -class InMemoryTests { - private val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) - - private fun inspect (b: SerializedBytes<*>) { - BlobHandler.make( - InMemoryConfig(Mode.inMem).apply { blob = b; data = true} - ).apply { - inspectBlob(config, getBytes()) - } - } - - @Test - fun test1() { - data class C (val a: Int, val b: Long, val c: String) - inspect (SerializationOutput(factory).serialize(C(100, 567L, "this is a test"), AMQP_P2P_CONTEXT)) - } - - @Test - fun test2() { - data class C (val i: Int, val c: C?) - inspect (SerializationOutput(factory).serialize(C(1, C(2, C(3, C(4, null)))), AMQP_P2P_CONTEXT)) - } - - @Test - fun test3() { - data class C (val a: IntArray, val b: Array) - - val a = IntArray(10) { i -> i } - val c = C(a, arrayOf("aaa", "bbb", "ccc")) - - inspect (SerializationOutput(factory).serialize(c, AMQP_P2P_CONTEXT)) - } - - @Test - fun test4() { - data class Elem(val e1: Long, val e2: String) - data class Wrapper (val name: String, val elementes: List) - - inspect (SerializationOutput(factory).serialize( - Wrapper("Outer Class", - listOf( - Elem(1L, "First element"), - Elem(2L, "Second element"), - Elem(3L, "Third element") - )), AMQP_P2P_CONTEXT)) - } - - @Test - fun test4b() { - data class Elem(val e1: Long, val e2: String) - data class Wrapper (val name: String, val elementes: List>) - - inspect (SerializationOutput(factory).serialize( - Wrapper("Outer Class", - listOf ( - listOf( - Elem(1L, "First element"), - Elem(2L, "Second element"), - Elem(3L, "Third element") - ), - listOf( - Elem(4L, "Fourth element"), - Elem(5L, "Fifth element"), - Elem(6L, "Sixth element") - ) - )), AMQP_P2P_CONTEXT)) - } - - @Test - fun test5() { - data class C (val a: Map) - - inspect (SerializationOutput(factory).serialize( - C(mapOf( - "a" to "a a a", - "b" to "b b b", - "c" to "c c c")), - AMQP_P2P_CONTEXT - )) - } -} \ No newline at end of file diff --git a/experimental/blobinspector/src/test/kotlin/net/corda/blobinspector/ModeParse.kt b/experimental/blobinspector/src/test/kotlin/net/corda/blobinspector/ModeParse.kt deleted file mode 100644 index 80560576a4..0000000000 --- a/experimental/blobinspector/src/test/kotlin/net/corda/blobinspector/ModeParse.kt +++ /dev/null @@ -1,83 +0,0 @@ -package net.corda.blobinspector - -import org.junit.Test -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import kotlin.test.assertFalse - -class ModeParse { - @Test - fun fileIsSetToFile() { - val opts1 = Array(2) { - when (it) { - 0 -> "-m" - 1 -> "file" - else -> "error" - } - } - - assertEquals(Mode.file, getMode(opts1).mode) - } - - @Test - fun nothingIsSetToFile() { - val opts1 = Array(0) { "" } - - assertEquals(Mode.file, getMode(opts1).mode) - } - - @Test - fun filePathIsSet() { - val opts1 = Array(4) { - when (it) { - 0 -> "-m" - 1 -> "file" - 2 -> "-f" - 3 -> "path/to/file" - else -> "error" - } - } - - val config = getMode(opts1) - assertTrue(config is FileConfig) - assertEquals(Mode.file, config.mode) - assertEquals("unset", (config as FileConfig).file) - - loadModeSpecificOptions(config, opts1) - - assertEquals("path/to/file", config.file) - } - - @Test - fun schemaIsSet() { - Array(2) { - when (it) { 0 -> "-f"; 1 -> "path/to/file"; else -> "error" - } - }.let { options -> - getMode(options).apply { - loadModeSpecificOptions(this, options) - assertFalse(schema) - } - } - - Array(3) { - when (it) { 0 -> "--schema"; 1 -> "-f"; 2 -> "path/to/file"; else -> "error" - } - }.let { - getMode(it).apply { - loadModeSpecificOptions(this, it) - assertTrue(schema) - } - } - - Array(3) { - when (it) { 0 -> "-f"; 1 -> "path/to/file"; 2 -> "-s"; else -> "error" - } - }.let { - getMode(it).apply { - loadModeSpecificOptions(this, it) - assertTrue(schema) - } - } - } -} \ No newline at end of file diff --git a/experimental/blobinspector/src/test/kotlin/net/corda/blobinspector/SimplifyClassTests.kt b/experimental/blobinspector/src/test/kotlin/net/corda/blobinspector/SimplifyClassTests.kt deleted file mode 100644 index 3dcafbc88d..0000000000 --- a/experimental/blobinspector/src/test/kotlin/net/corda/blobinspector/SimplifyClassTests.kt +++ /dev/null @@ -1,28 +0,0 @@ -package net.corda.blobinspector - -import org.junit.Test - -class SimplifyClassTests { - - @Test - fun test1() { - data class A(val a: Int) - - println(A::class.java.name) - println(A::class.java.name.simplifyClass()) - } - - @Test - fun test2() { - val p = this.javaClass.`package`.name - - println("$p.Class1<$p.Class2>") - println("$p.Class1<$p.Class2>".simplifyClass()) - println("$p.Class1<$p.Class2, $p.Class3>") - println("$p.Class1<$p.Class2, $p.Class3>".simplifyClass()) - println("$p.Class1<$p.Class2<$p.Class3>>") - println("$p.Class1<$p.Class2<$p.Class3>>".simplifyClass()) - println("$p.Class1<$p.Class2<$p.Class3>>") - println("$p.Class1\$C<$p.Class2<$p.Class3>>".simplifyClass()) - } -} \ No newline at end of file diff --git a/experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.1Composite b/experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.1Composite deleted file mode 100644 index 450e6970da613f518ebafb1bdf71cb8c212f4a05..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 537 zcmYe!FG@*dWME)uIGO|`85kHZFfcGN0U68;i4ypq(Syu=*6;?$zd#GK5k#FEVXJiWx+ z!UDaJ)Z!9D=bXgiVh2~aLCgo#n27O&!vTLrxF(hZPV5UA6BnA3?f@gtypm#92M0%l wKUoi~WF^}uJYK;S&JG7G5dq0^KmhC&W?;y5b13o`XG^GZ^S@)C3Oic^a+ z6LT`F5=%1k^YjvP3k&o@Qj1FrJ@ZN&T;awrA5dc=#xV{DWD$0=91vh%$e6g08EBd7 QLPi4zM<=*}oCg^h04>={1poj5 diff --git a/experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.1String b/experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.1String deleted file mode 100644 index 9676f0375fb488e6ec107de206caa34f9cc19f96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 262 zcmYe!FG@*dWME)uIGO|`85kH3d}3x;tdy5pqL&Pkv+{H_c1zbUjtomPFg6M^G4%>b zEwga6wLKunxR5U-BePfmh!Pb_Qj1IAhARf#2;`FVPYxrGIKAwUBSgG-7s^U@t$;U+O3P-7y-bq)vQ e5Z1FC5M*D-n7ELw7;K!YgM*_J+(gcUj0^y)B2T9P diff --git a/experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.2Composite b/experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.2Composite deleted file mode 100644 index 0bf3a5c47551253abc26f5aa0b23a67d9549e7b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1004 zcmbtTK~KUk7;VRf@#eu8y?Wpv$y5jeM$87xC=r!_LA{kRurM~Hm4PRJf`4J*Ieb=9$#yG2LQeS5PIi_5EyM*29 z3H6-_+==k{ZAtjx2hp!dyaw0#mE@*V-EUGiin7f^6wQKBh|nw?-f=4>@L^LMu0Q=VIy z_OR6qa!Tx3T3FtkcxFwB!qSuRtc>Fc@pfn~+zFcXYBl$#+F11eQkWxs*3c>}4kvmM w8B2#-Yy-M$E!0QyJ;kQ%1${uqNgoEf7iN|4OrSP3+0)ZD>w`?#Sbl-v3;zg0-v9sr diff --git a/experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.2Int b/experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.2Int deleted file mode 100644 index 118a23f37b86208a302c0d3be55efaf86673c3d3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 284 zcmYe!FG@*dWME)uIGO|`85kHZ05L0&!OXB&DKE7|FBzo5D%mMHEj!9ItvIbB(#s{# zsKV6OT;I{w_5cS{NJ=44`3blQOb2!{9@xeR=d&JI$-2-SZj@eLeoAU$L8e}2UP)?E zUSf`3acWU!VoqjNVo7Fxo?c>ZVS!#qYH^8?XI@FMgDcz|<^yU>#5l>}fF%=L6UzYs d_Jxdz3z>noxh`ZhaBy@&lS{%X$9a&E0RTlfQnUa7 diff --git a/experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.3Int b/experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.3Int deleted file mode 100644 index 9f00d5906853f44dcc3d7aecab4b8b5babbfbae4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 317 zcmYe!FG@*dWME)uIGO|`85kHZ0I?d7!OXB&DKE7|FBzo5s@%0S(#A+RS1D6=#eAWX;Sr?ka&C|=vPf1NI$kfZs zD@iTNOU%(LPA$qz%*m`uEXmBz(@V@PEYJ%{EiN(k%quB&aD|)0d_awf7ZWQwYH6~&_<8Z)$5w3~lfHM0+#>9n;S_i}!7Yb)3 zmL=+ymSpDWfy}o7y28%E!O;n>ob5m$8+KK$3t4i2THq?tU4tvgkUYS7kdXlZo6mpj diff --git a/experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.MapIntClass b/experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.MapIntClass deleted file mode 100644 index 175949d9aab8cdbb3b545e805a3b620ef6ab854e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 759 zcmYe!FG@*dWME)uIGO|`85kHZFfcGZ05X^v7AxhYmgpseR9KZ{dbos^Iad?~1O)n; zM&zYr78e$m+u9xoXGBp~&_q_IkiQ{+;0|H}Imij-GE1<_V2))y@PKupE!>rQdHE@+ zi3OQ@nRz9tMR|!idc~Rf#2;`FVPYxrGIKA*sbBzKI2%c_q#{iN(bZu5g=} z52!H_<7bBhS&VQ^EC*uQ7c%E27A$1cI^f5+&^s%!EK#qtBr``3Xp0TdB{~YEJJ1N| zyka{C2S+EkMQjH;*r;o|E7;v|3(&ofD=LvfjP<}u)`jL|dJoAQbgOZt4Tl4kOmO$I f91vh%$e6g085qW{3mFZN0}CRTgjJ67AR_|+;<@xA diff --git a/experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.MapIntString b/experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.MapIntString deleted file mode 100644 index 67ba352ec48a9e7a47412b7580915babc19fa383..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 474 zcmYe!FG@*dWME)uIGO|`85kHZ0P$fUgPCEmQeJ9_UNT69l|^x&n{k1qsc%I@h^uR% zk4I*jS5&#J?STMB6m`BXxrS*bMHQ8v7NOzBu8!tz-X2NmwzdZ~*g`}WvL-43VTi^; zj-(_7FbZK`$XAk)S*!p=i3%mD#U*f;Fo9g619Orxkii6(V?D5eb)g;Hd3t&IDXEDC znR=OdC8$f$KdmT{p}R$^JAUTH~Yjvmk&8=zBk6pFz%**Q2k fI>Gg^9SCQ`tJoE6DqKCf<8TErl7Bc4GBN-FYQBy% diff --git a/experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.StringList b/experimental/blobinspector/src/test/resources/net/corda/blobinspector/FileParseTests.StringList deleted file mode 100644 index 5758d9fa62f3e815bac4b1ba1e6af87c07f22e02..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 448 zcmYe!FG@*dWME)uIGO|`85kHZ0P#v7gPCEmQeJ9_UNT69m8)aEV|Z|QcBn;8R+@HY zZenGHMM=7??Ewcy6m51}u@Al?FG5Vr7}RJ&q{f&iQ4+(8C{bffG=Zr9Y;`-kI)#StGM(=E z=D&Ua_y0#T%nb4gg7C~oX?O~PZ%Gi=Uxt@}u-Hg3lQjqNfa7^fqH= zSKK_1-LnVK(se-ltI#vFYdWv0X5T_vegMyQV5`u*|KKvZ-f?mYK3i9|TwkQQ zbDb0OXl&k^=uh^yCJr4?2ZoxLefwg74#lfujppZKLlZND(PYQ%2-aYjV1+yQDj~hLEE04I>6aW~pHen{!0NJd`S!M4MK)zdtH0Zd)n! z44Ol-g*7;4C3&=v)`>QVOD7*|+1Vn70!=s{*e0C+>yJa%qd%Wl>pC{E_tIZZUYaa) z`zNn$FzPOTcjgNzd;}Q$zWfjtAE+_TNDj*}YB5BaE&3BeT!{NB9pDE}#it#XoR@KU z5i4gR{N#Jb`H6Q_jLOu=V3a2^p$wZ8=`tJ@4Rcl=S^hhUOq9cLW+`gLRKR}N@9r=! zmKDckuS0X?)1bXW;`inVCAmUwR7Y&eFmvNzo*0w|APSZHT9m%9>w~z&>!?Ej!4~)J zv=txUQ!aFY5nUc47VEj0e99EY=#5c%V4D!A+GT8CabR&3(0L>aW3p#?|=QwFl)TUVUJ7{c~p;<1a4MWF>s$85xx`o}XAljdcH?Y1+Dqf>Tv5&e_X_t7Af~5EkuQd9d zsh#X>X&C`nZ8}hibw-gwDlk_ZHEi+m%^qW?s=dnckCf$qbDmd8)oWTR_GP!yw?OGN zBbPI%lL-%5%!Ifhqp(}v4HDmIm{~-L1r!-HK?(E1J_w?6V_mqak#QgNi*9H@JM_zn z-zv-|P#cU<<{8*#iA$PMpv?5JvF-!dfbKb%jx{G^bpPl^;&^4$q!_y>wyk z@6VyDXDWF6Lej}HM?U~lGYd@7^aTXV0 zu+C&Bi!pqpfT@OiD>bj3l3sT*#co{L{XyI5=(JN79!e7m7na(jPK&n`-|b5zu2tMEkn9R#I~VKfKPqv5dQD^chAcWm=Wk|j B^OXPq diff --git a/node/build.gradle b/node/build.gradle index eff17b13c8..2a0aabcd4d 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -98,7 +98,7 @@ dependencies { compile "org.fusesource.jansi:jansi:$jansi_version" // Manifests: for reading stuff from the manifest file - compile "com.jcabi:jcabi-manifests:1.1" + compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version" compile("com.intellij:forms_rt:7.0.3") { exclude group: "asm" diff --git a/node/src/main/kotlin/net/corda/node/serialization/kryo/CordaClassResolver.kt b/node/src/main/kotlin/net/corda/node/serialization/kryo/CordaClassResolver.kt index a826aac113..58e8b61c06 100644 --- a/node/src/main/kotlin/net/corda/node/serialization/kryo/CordaClassResolver.kt +++ b/node/src/main/kotlin/net/corda/node/serialization/kryo/CordaClassResolver.kt @@ -8,13 +8,12 @@ import com.esotericsoftware.kryo.util.DefaultClassResolver import com.esotericsoftware.kryo.util.Util import net.corda.core.internal.writer import net.corda.core.serialization.ClassWhitelist -import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializationContext import net.corda.core.utilities.contextLogger import net.corda.serialization.internal.AttachmentsClassLoader import net.corda.serialization.internal.MutableClassWhitelist import net.corda.serialization.internal.TransientClassWhiteList -import net.corda.serialization.internal.amqp.hasAnnotationInHierarchy +import net.corda.serialization.internal.amqp.hasCordaSerializable import java.io.PrintWriter import java.lang.reflect.Modifier import java.lang.reflect.Modifier.isAbstract @@ -127,7 +126,7 @@ class CordaClassResolver(serializationContext: SerializationContext) : DefaultCl return (type.classLoader !is AttachmentsClassLoader) && !KryoSerializable::class.java.isAssignableFrom(type) && !type.isAnnotationPresent(DefaultSerializer::class.java) - && (type.isAnnotationPresent(CordaSerializable::class.java) || whitelist.hasAnnotationInHierarchy(type)) + && hasCordaSerializable(type) } // Need to clear out class names from attachments. diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt index 9a929e9df3..47b5387413 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt @@ -525,14 +525,17 @@ fun ClassWhitelist.requireWhitelisted(type: Type) { } } -fun ClassWhitelist.isWhitelisted(clazz: Class<*>) = (hasListed(clazz) || hasAnnotationInHierarchy(clazz)) -fun ClassWhitelist.isNotWhitelisted(clazz: Class<*>) = !(this.isWhitelisted(clazz)) +fun ClassWhitelist.isWhitelisted(clazz: Class<*>) = hasListed(clazz) || hasCordaSerializable(clazz) +fun ClassWhitelist.isNotWhitelisted(clazz: Class<*>) = !this.isWhitelisted(clazz) -// Recursively check the class, interfaces and superclasses for our annotation. -fun ClassWhitelist.hasAnnotationInHierarchy(type: Class<*>): Boolean { +/** + * Check the given [Class] has the [CordaSerializable] annotation, either directly or inherited from any of its super + * classes or interfaces. + */ +fun hasCordaSerializable(type: Class<*>): Boolean { return type.isAnnotationPresent(CordaSerializable::class.java) - || type.interfaces.any { hasAnnotationInHierarchy(it) } - || (type.superclass != null && hasAnnotationInHierarchy(type.superclass)) + || type.interfaces.any(::hasCordaSerializable) + || (type.superclass != null && hasCordaSerializable(type.superclass)) } /** @@ -555,27 +558,28 @@ fun ClassWhitelist.hasAnnotationInHierarchy(type: Class<*>): Boolean { * * As such, if objectInstance fails access, revert to Java reflection and try that */ -fun Class<*>.objectInstance() = - try { - this.kotlin.objectInstance - } catch (e: IllegalAccessException) { - // Check it really is an object (i.e. it has no constructor) - if (constructors.isNotEmpty()) null - else { - try { - this.getDeclaredField("INSTANCE")?.let { field -> - // and must be marked as both static and final (>0 means they're set) - if (modifiers and Modifier.STATIC == 0 || modifiers and Modifier.FINAL == 0) null - else { - val accessibility = field.isAccessible - field.isAccessible = true - val obj = field.get(null) - field.isAccessible = accessibility - obj - } +fun Class<*>.objectInstance(): Any? { + return try { + this.kotlin.objectInstance + } catch (e: IllegalAccessException) { + // Check it really is an object (i.e. it has no constructor) + if (constructors.isNotEmpty()) null + else { + try { + this.getDeclaredField("INSTANCE")?.let { field -> + // and must be marked as both static and final (>0 means they're set) + if (modifiers and Modifier.STATIC == 0 || modifiers and Modifier.FINAL == 0) null + else { + val accessibility = field.isAccessible + field.isAccessible = true + val obj = field.get(null) + field.isAccessible = accessibility + obj } - } catch (e: NoSuchFieldException) { - null } + } catch (e: NoSuchFieldException) { + null } } + } +} diff --git a/settings.gradle b/settings.gradle index 313696cd0b..a597018e24 100644 --- a/settings.gradle +++ b/settings.gradle @@ -36,6 +36,7 @@ include 'tools:demobench' include 'tools:loadtest' include 'tools:graphs' include 'tools:bootstrapper' +include 'tools:blobinspector' include 'tools:shell' include 'example-code' project(':example-code').projectDir = file("$settingsDir/docs/source/example-code") diff --git a/tools/blobinspector/build.gradle b/tools/blobinspector/build.gradle new file mode 100644 index 0000000000..5d49461034 --- /dev/null +++ b/tools/blobinspector/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'java' +apply plugin: 'kotlin' + +dependencies { + compile project(':client:jackson') + compile 'info.picocli:picocli:3.0.0' + compile "org.slf4j:slf4j-nop:$slf4j_version" + compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version" + + testCompile project(':test-utils') + testCompile "junit:junit:$junit_version" +} + +jar { + from(configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }) { + exclude "META-INF/*.SF" + exclude "META-INF/*.DSA" + exclude "META-INF/*.RSA" + } + baseName 'blobinspector' + manifest { + attributes( + 'Automatic-Module-Name': 'net.corda.blobinspector', + 'Main-Class': 'net.corda.blobinspector.MainKt' + ) + } +} diff --git a/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/Main.kt b/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/Main.kt new file mode 100644 index 0000000000..e2bde0639e --- /dev/null +++ b/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/Main.kt @@ -0,0 +1,125 @@ +package net.corda.blobinspector + +import com.fasterxml.jackson.core.JsonFactory +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import com.jcabi.manifests.Manifests +import net.corda.client.jackson.JacksonSupport +import net.corda.core.internal.rootMessage +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.internal.SerializationEnvironmentImpl +import net.corda.core.serialization.internal._contextSerializationEnv +import net.corda.core.utilities.sequence +import net.corda.serialization.internal.AMQP_P2P_CONTEXT +import net.corda.serialization.internal.CordaSerializationMagic +import net.corda.serialization.internal.SerializationFactoryImpl +import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme +import net.corda.serialization.internal.amqp.DeserializationInput +import net.corda.serialization.internal.amqp.amqpMagic +import picocli.CommandLine +import picocli.CommandLine.* +import java.net.MalformedURLException +import java.net.URL +import java.nio.file.Paths +import kotlin.system.exitProcess + +fun main(args: Array) { + val main = Main() + try { + CommandLine.run(main, *args) + } catch (e: ExecutionException) { + val throwable = e.cause ?: e + if (main.verbose) { + throwable.printStackTrace() + } else { + System.err.println("*ERROR*: ${throwable.rootMessage ?: "Use --verbose for more details"}") + } + exitProcess(1) + } +} + +@Command( + name = "Blob Inspector", + versionProvider = VersionProvider::class, + mixinStandardHelpOptions = true, // add --help and --version options, + showDefaultValues = true, + description = ["Inspect AMQP serialised binary blobs"] +) +class Main : Runnable { + @Parameters(index = "0", paramLabel = "SOURCE", description = ["URL or file path to the blob"], converter = [SourceConverter::class]) + private var source: URL? = null + + @Option(names = ["--format"], paramLabel = "type", description = ["Output format. Possible values: [YAML, JSON]"]) + private var formatType: FormatType = FormatType.YAML + + @Option(names = ["--full-parties"], + description = ["Display the owningKey and certPath properties of Party and PartyAndReference objects respectively"]) + private var fullParties: Boolean = false + + @Option(names = ["--schema"], description = ["Print the blob's schema first"]) + private var schema: Boolean = false + + @Option(names = ["--verbose"], description = ["Enable verbose output"]) + var verbose: Boolean = false + + override fun run() { + val bytes = source!!.readBytes().run { + require(size > amqpMagic.size) { "Insufficient bytes for AMQP blob" } + sequence() + } + + require(bytes.take(amqpMagic.size) == amqpMagic) { "Not an AMQP blob" } + + if (schema) { + val envelope = DeserializationInput.getEnvelope(bytes) + println(envelope.schema) + println() + } + + initialiseSerialization() + + val factory = when (formatType) { + FormatType.YAML -> YAMLFactory() + FormatType.JSON -> JsonFactory() + } + val mapper = JacksonSupport.createNonRpcMapper(factory, fullParties) + + val deserialized = bytes.deserialize() + println(deserialized.javaClass.name) + mapper.writeValue(System.out, deserialized) + } + + private fun initialiseSerialization() { + _contextSerializationEnv.set(SerializationEnvironmentImpl( + SerializationFactoryImpl().apply { + registerScheme(AMQPInspectorSerializationScheme) + }, + AMQP_P2P_CONTEXT + )) + } +} + +private object AMQPInspectorSerializationScheme : AbstractAMQPSerializationScheme(emptyList()) { + override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean { + return magic == amqpMagic && target == SerializationContext.UseCase.P2P + } + override fun rpcClientSerializerFactory(context: SerializationContext) = throw UnsupportedOperationException() + override fun rpcServerSerializerFactory(context: SerializationContext) = throw UnsupportedOperationException() +} + +private class SourceConverter : ITypeConverter { + override fun convert(value: String): URL { + return try { + URL(value) + } catch (e: MalformedURLException) { + Paths.get(value).toUri().toURL() + } + } +} + +private class VersionProvider : IVersionProvider { + override fun getVersion(): Array = arrayOf(Manifests.read("Corda-Release-Version")) +} + +private enum class FormatType { YAML, JSON } +