diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index c77a2ba99d..45f5e01185 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -61,12 +61,14 @@ see changes to this list. * cncorda * Credit Suisse * cyrsis +* Dan Newton (Accenture) * Daniel Roig (SEB) * Dave Hudson (R3) * David John Grundy (Dankse Bank) * David Lee (BCS) * Dirk Hermans (KBC) * Edward Greenwood (State Street) +* Elendu Uche (APPZONE) * Farzad Pezeshkpour (RBS) * fracting * Frederic Dalibard (Natixis) diff --git a/build.gradle b/build.gradle index 093e26d750..0d80541da0 100644 --- a/build.gradle +++ b/build.gradle @@ -91,6 +91,7 @@ buildscript { ext.ghostdriver_version = '2.1.0' ext.eaagentloader_version = '1.0.3' ext.curator_version = '4.0.0' + ext.proguard_version = constants.getProperty('proguardVersion') ext.jsch_version = '0.1.54' ext.protonj_version = '0.27.1' ext.commons_cli_version = '1.4' @@ -98,6 +99,8 @@ buildscript { ext.fast_classpath_scanner_version = '2.12.3' ext.jcabi_manifests_version = '1.1' + ext.deterministic_rt_version = '1.0-SNAPSHOT' + // Update 121 is required for ObjectInputFilter and at time of writing 131 was latest: ext.java8_minUpdateVersion = '131' @@ -122,6 +125,7 @@ buildscript { classpath "net.corda.plugins:cordformation:$gradle_plugins_version" classpath "net.corda.plugins:cordapp:$gradle_plugins_version" classpath "net.corda.plugins:api-scanner:$gradle_plugins_version" + classpath "net.sf.proguard:proguard-gradle:$proguard_version" classpath 'com.github.ben-manes:gradle-versions-plugin:0.15.0' classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}" @@ -160,7 +164,6 @@ targetCompatibility = 1.8 allprojects { apply plugin: 'kotlin' - apply plugin: 'java' apply plugin: 'jacoco' apply plugin: 'org.owasp.dependencycheck' apply plugin: 'kotlin-allopen' @@ -208,7 +211,7 @@ allprojects { tasks.withType(Test) { // Prevent the project from creating temporary files outside of the build directory. - systemProperties['java.io.tmpdir'] = buildDir + systemProperty 'java.io.tmpdir', buildDir.absolutePath // Ensures that "net.corda.testing.amqp.enable" is passed correctly from Gradle command line // down to JVM executing unit test. It looks like we are running unit tests in the forked mode @@ -310,6 +313,7 @@ if (!JavaVersion.current().java8Compatible) throw new GradleException("Corda requires Java 8, please upgrade to at least 1.8.0_$java8_minUpdateVersion") repositories { + mavenLocal() mavenCentral() jcenter() } @@ -380,7 +384,34 @@ bintrayConfig { projectUrl = 'https://github.com/corda/corda' gpgSign = true gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE') - publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver', 'corda-node-driver', 'corda-confidential-identities', 'corda-launcher', 'corda-shell', 'corda-serialization', 'corda-bridgeserver', 'corda-ptflows', 'jmeter-corda', 'tools-database-migration', 'tools-network-bootstrapper', 'tools-blob-inspector'] + publications = [ + 'corda-jfx', + 'corda-mock', + 'corda-rpc', + 'corda-core', + 'corda-core-deterministic', + 'corda', + 'corda-finance', + 'corda-node', + 'corda-node-api', + 'corda-test-common', + 'corda-test-utils', + 'corda-jackson', + 'corda-webserver-impl', + 'corda-webserver', + 'corda-node-driver', + 'corda-confidential-identities', + 'corda-launcher', + 'corda-shell', + 'corda-serialization', + 'corda-serialization-deterministic', + 'corda-bridgeserver', + 'corda-ptflows', + 'jmeter-corda', + 'tools-database-migration', + 'tools-blob-inspector', + 'tools-network-bootstrapper' + ] license { name = 'Apache-2.0' url = 'https://www.apache.org/licenses/LICENSE-2.0' diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 792439dacd..cfbdd7722c 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -12,7 +12,24 @@ buildscript { Properties constants = new Properties() file("../constants.properties").withInputStream { constants.load(it) } - ext.guava_version = constants.getProperty("guavaVersion") + ext { + guava_version = constants.getProperty("guavaVersion") + kotlin_version = constants.getProperty("kotlinVersion") + proguard_version = constants.getProperty("proguardVersion") + assertj_version = '3.9.1' + junit_version = '4.12' + asm_version = '6.2' + } + + repositories { + mavenLocal() + mavenCentral() + jcenter() + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "net.sf.proguard:proguard-gradle:$proguard_version" + } } apply plugin: 'maven' @@ -23,6 +40,27 @@ repositories { mavenCentral() } +allprojects { + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + languageVersion = "1.2" + apiVersion = "1.2" + jvmTarget = "1.8" + javaParameters = true // Useful for reflection. + } + } + + tasks.withType(Test) { + // Prevent the project from creating temporary files outside of the build directory. + systemProperty 'java.io.tmpdir', buildDir.absolutePath + + // Tell the tests where Gradle's current module cache is. + // We need the tests to share this module cache to prevent the + // Gradle Test-Kit from downloading its own copy of Kotlin etc. + systemProperty 'test.gradle.user.home', project.gradle.gradleUserHomeDir + } +} + dependencies { // Add the top-level projects ONLY to the host project. runtime project.childProjects.values().collect { diff --git a/buildSrc/jarfilter/README.md b/buildSrc/jarfilter/README.md new file mode 100644 index 0000000000..a7a0546816 --- /dev/null +++ b/buildSrc/jarfilter/README.md @@ -0,0 +1,152 @@ +# JarFilter + +Deletes annotated elements at the byte-code level from a JAR of Java/Kotlin code. In the case of Kotlin +code, it also modifies the `@kotlin.Metadata` annotations not to contain any functions, properties or +type aliases that have been deleted. This prevents the Kotlin compiler from successfully compiling against +any elements which no longer exist. + +We use this plugin together with ProGuard to generate Corda's `core-deterministic` and `serialization-deterministic` +modules. See [here](../../docs/source/deterministic-modules.rst) for more information. + +## Usage +This plugin is automatically available on Gradle's classpath since it lives in Corda's `buildSrc` directory. +You need only `import` the plugin's task classes in the `build.gradle` file and then use them to declare +tasks. + +You can enable the tasks' logging output using Gradle's `--info` or `--debug` command-line options. + +### The `JarFilter` task +The `JarFilter` task removes unwanted elements from `class` files, namely: +- Deleting both Java methods/fields and Kotlin functions/properties/type aliases. +- Stubbing out methods by replacing the byte-code of their implementations. +- Removing annotations from classes/methods/fields. + +It supports the following configuration options: +```gradle +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + // Task(s) whose JAR outputs should be filtered. + jars jar + + // The annotations assigned to each filtering role. For example: + annotations { + forDelete = [ + "org.testing.DeleteMe" + ] + forStub = [ + "org.testing.StubMeOut" + ] + forRemove = [ + "org.testing.RemoveMe" + ] + } + + // Location for filtered JARs. Defaults to "$buildDir/filtered-libs". + outputDir file(...) + + // Whether the timestamps on the JARs' entries should be preserved "as is" + // or set to a platform-independent constant value (1st February 1980). + preserveTimestamps = {true|false} + + // The maximum number of times (>= 1) to pass the JAR through the filter. + maxPasses = 5 + + // Writes more information about each pass of the filter. + verbose = {true|false} +} +``` + +You can specify as many annotations for each role as you like. The only constraint is that a given +annotation cannot be assigned to more than one role. + +### The `MetaFixer` task +The `MetaFixer` task updates the `@kotlin.Metadata` annotations by removing references to any functions, +constructors, properties or nested classes that no longer exist in the byte-code. This is primarily to +"repair" Kotlin library code that has been processed by ProGuard. + +Kotlin type aliases exist only inside `@Metadata` and so are unaffected by this task. Similarly, the +constructors for Kotlin's annotation classes don't exist in the byte-code either because Java annotations +are interfaces really. The `MetaFixer` task will therefore ignore annotations' constructors too. + +It supports these configuration options: +```gradle +import net.corda.gradle.jarfilter.MetaFixerTask +task metafix(type: MetaFixerTask) { + // Task(s) whose JAR outputs should be fixed. + jars jar + + // Location for fixed JARs. Defaults to "$buildDir/metafixed-libs" + outputDir file(...) + + // Tag to be appended to the JAR name. Defaults to "-metafixed". + suffix = "..." + + // Whether the timestamps on the JARs' entries should be preserved "as is" + // or set to a platform-independent constant value (1st February 1980). + preserveTimestamps = {true|false} +} +``` + +## Implementation Details + +### Code Coverage +You can generate a JaCoCo code coverage report for the unit tests using: +```bash +$ cd buildSrc +$ ../gradlew jarfilter:jacocoTestReport +``` + +### Kotlin Metadata +The Kotlin compiler encodes information about each class inside its `@kotlin.Metadata` annotation. + +```kotlin +import kotlin.annotation.AnnotationRetention.* + +@Retention(RUNTIME) +annotation class Metadata { + val k: Int = 1 + val d1: Array = [] + val d2: Array = [] + // ... +} +``` + +This is an internal feature of Kotlin which is read by Kotlin Reflection. There is no public API +for writing this information, and the content format of arrays `d1` and `d2` depends upon the +"class kind" `k`. For the kinds that we are interested in, `d1` contains a buffer of ProtoBuf +data and `d2` contains an array of `String` identifiers which the ProtoBuf data refers to by index. + +Although ProtoBuf generates functions for both reading and writing the data buffer, the +Kotlin Reflection artifact only contains the functions for reading. This is almost certainly +because the writing functionality has been removed from the `kotlin-reflect` JAR using +ProGuard. However, the complete set of generated ProtoBuf classes is still available in the +`kotlin-compiler-embeddable` JAR. The `jarfilter:kotlin-metadata` module uses ProGuard to +extracts these classes into a new `kotlin-metdata` JAR, discarding any classes that the +ProtoBuf ones do not need and obfuscating any other ones that they do. + +The custom `kotlin-metadata` object was originally created as a workaround for +[KT-18621](https://youtrack.jetbrains.com/issue/KT-18621). However, reducing the number of unwanted +classes on the classpath anyway can only be a Good Thing(TM). + +At runtime, `JarFilter` decompiles the ProtoBuf buffer into POJOs, deletes the elements that +no longer exist in the byte-code and then recompiles the POJOs into a new ProtoBuf buffer. The +`@Metadata` annotation is then rewritten using this new buffer for `d1` and the _original_ `String` +identifiers for `d2`. While some of these identifiers are very likely no longer used after this, +removing them would also require re-indexing the ProtoBuf data. It is therefore simpler just to +leave them as harmless cruft in the byte-code's constant pool. + +The majority of `JarFilter`'s unit tests use Kotlin and Java reflection and so should not be +brittle as Kotlin evolves because `kotlin-reflect` is public API. Also, Kotlin's requirement that +it remain backwards-compatible with itself should imply that the ProtoBuf logic shouldn't change +(much). However, the ProtoBuf classes are still internal to Kotlin and so it _is_ possible that they +will occasionally move between packages. This has already happened for Kotlin 1.2.3x -> 1.2.4x, but +I am hoping this means that they will not move again for a while. + +### JARs vs ZIPs +The `JarFilter` and `MetaFixer` tasks _deliberately_ use `ZipFile` and `ZipOutputStream` rather +than `JarInputStream` and `JarOutputStream` when reading and writing their JAR files. This is to +ensure that the original `META-INF/MANIFEST.MF` files are passed through unaltered. Note also that +there is no `ZipInputStream.getComment()` method, and so we need to use `ZipFile` in order to +preserve any JAR comments. + +Neither `JarFilter` nor `MetaFixer` should change the order of the entries inside the JAR files. diff --git a/buildSrc/jarfilter/build.gradle b/buildSrc/jarfilter/build.gradle new file mode 100644 index 0000000000..f4067d6e7a --- /dev/null +++ b/buildSrc/jarfilter/build.gradle @@ -0,0 +1,46 @@ +plugins { + id 'java-gradle-plugin' + id 'jacoco' +} +apply plugin: 'kotlin' + +repositories { + mavenLocal() + mavenCentral() + jcenter() +} + +gradlePlugin { + plugins { + jarFilterPlugin { + id = 'net.corda.plugins.jar-filter' + implementationClass = 'net.corda.gradle.jarfilter.JarFilterPlugin' + } + } +} + +configurations { + jacocoRuntime +} + +processTestResources { + filesMatching('**/build.gradle') { + expand(['kotlin_version': kotlin_version]) + } + filesMatching('gradle.properties') { + expand(['jacocoAgent': configurations.jacocoRuntime.asPath.replace('\\', '/'), + 'buildDir': buildDir]) + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + implementation project(':jarfilter:kotlin-metadata') + implementation "org.ow2.asm:asm:$asm_version" + testImplementation "org.jetbrains.kotlin:kotlin-test-junit" + testImplementation "org.jetbrains.kotlin:kotlin-reflect" + testImplementation "org.assertj:assertj-core:$assertj_version" + testImplementation "junit:junit:$junit_version" + testImplementation project(':jarfilter:unwanteds') + jacocoRuntime "org.jacoco:org.jacoco.agent:${jacoco.toolVersion}:runtime" +} diff --git a/buildSrc/jarfilter/kotlin-metadata/build.gradle b/buildSrc/jarfilter/kotlin-metadata/build.gradle new file mode 100644 index 0000000000..b509187551 --- /dev/null +++ b/buildSrc/jarfilter/kotlin-metadata/build.gradle @@ -0,0 +1,81 @@ +plugins { + id 'base' +} + +description "Kotlin's metadata-handling classes" + +repositories { + mavenLocal() + jcenter() +} + +configurations { + proguard + runtime + configurations.default.extendsFrom runtime +} + +dependencies { + proguard "org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlin_version" + proguard "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + runtime "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" +} + +def javaHome = System.getProperty('java.home') +def originalJar = configurations.proguard.files.find { it.name.startsWith("kotlin-compiler-embeddable") } + +import proguard.gradle.ProGuardTask +task metadata(type: ProGuardTask) { + injars originalJar, filter: '!META-INF/native/**' + outjars "$buildDir/libs/${project.name}-${kotlin_version}.jar" + + libraryjars "$javaHome/lib/rt.jar" + libraryjars "$javaHome/../lib/tools.jar" + configurations.proguard.forEach { + if (originalJar != it) { + libraryjars it.path, filter: '!META-INF/versions/**' + } + } + + keepattributes '*' + dontoptimize + printseeds + verbose + + dontwarn 'com.sun.jna.**' + dontwarn 'org.jetbrains.annotations.**' + dontwarn 'org.jetbrains.kotlin.com.intellij.**' + dontwarn 'org.jetbrains.kotlin.com.google.j2objc.annotations.**' + dontwarn 'org.jetbrains.kotlin.com.google.errorprone.annotations.**' + + keep 'class org.jetbrains.kotlin.load.java.JvmAnnotationNames { *; }' + keep 'class org.jetbrains.kotlin.metadata.** { *; }', includedescriptorclasses: true + keep 'class org.jetbrains.kotlin.protobuf.** { *; }', includedescriptorclasses: true +} +def metadataJar = metadata.outputs.files.singleFile + +task validate(type: ProGuardTask) { + injars metadataJar + libraryjars "$javaHome/lib/rt.jar" + configurations.runtime.forEach { + libraryjars it.path, filter: '!META-INF/versions/**' + } + + keepattributes '*' + dontpreverify + dontobfuscate + dontoptimize + verbose + + dontwarn 'org.jetbrains.kotlin.com.google.errorprone.annotations.**' + + keep 'class *' +} + +artifacts { + 'default' file: metadataJar, name: project.name, type: 'jar', extension: 'jar', builtBy: metadata +} + +defaultTasks "metadata" +assemble.dependsOn metadata +metadata.finalizedBy validate diff --git a/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/ClassTransformer.kt b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/ClassTransformer.kt new file mode 100644 index 0000000000..8b181fed6c --- /dev/null +++ b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/ClassTransformer.kt @@ -0,0 +1,353 @@ +package net.corda.gradle.jarfilter + +import org.gradle.api.InvalidUserDataException +import org.gradle.api.logging.Logger +import org.objectweb.asm.* +import org.objectweb.asm.Opcodes.* + +/** + * ASM [ClassVisitor] for the JarFilter task that deletes unwanted class elements. + * The unwanted elements have been annotated in advance. Elements that reference + * unwanted elements are also removed to keep the byte-code consistent. Finally, + * the deleted elements are passed to the [MetadataTransformer] so that they can + * be removed from the [kotlin.Metadata] annotation. + * + * This Visitor is applied to the byte-code repeatedly until it has removed + * everything that is no longer wanted. + */ +class ClassTransformer private constructor ( + visitor: ClassVisitor, + logger: Logger, + kotlinMetadata: MutableMap>, + private val removeAnnotations: Set, + private val deleteAnnotations: Set, + private val stubAnnotations: Set, + private val unwantedClasses: MutableSet, + private val unwantedFields: MutableSet, + private val deletedMethods: MutableSet, + private val stubbedMethods: MutableSet +) : KotlinAwareVisitor(ASM6, visitor, logger, kotlinMetadata), Repeatable { + constructor( + visitor: ClassVisitor, + logger: Logger, + removeAnnotations: Set, + deleteAnnotations: Set, + stubAnnotations: Set, + unwantedClasses: MutableSet + ) : this( + visitor = visitor, + logger = logger, + kotlinMetadata = mutableMapOf(), + removeAnnotations = removeAnnotations, + deleteAnnotations = deleteAnnotations, + stubAnnotations = stubAnnotations, + unwantedClasses = unwantedClasses, + unwantedFields = mutableSetOf(), + deletedMethods = mutableSetOf(), + stubbedMethods = mutableSetOf() + ) + + private var _className: String = "(unknown)" + val className: String get() = _className + + val isUnwantedClass: Boolean get() = isUnwantedClass(className) + override val hasUnwantedElements: Boolean + get() = unwantedFields.isNotEmpty() + || deletedMethods.isNotEmpty() + || stubbedMethods.isNotEmpty() + || super.hasUnwantedElements + + private fun isUnwantedClass(name: String): Boolean = unwantedClasses.contains(name) + private fun hasDeletedSyntheticMethod(name: String): Boolean = deletedMethods.any { method -> + name.startsWith("$className\$${method.visibleName}\$") + } + + override fun recreate(visitor: ClassVisitor) = ClassTransformer( + visitor = visitor, + logger = logger, + kotlinMetadata = kotlinMetadata, + removeAnnotations = removeAnnotations, + deleteAnnotations = deleteAnnotations, + stubAnnotations = stubAnnotations, + unwantedClasses = unwantedClasses, + unwantedFields = unwantedFields, + deletedMethods = deletedMethods, + stubbedMethods = stubbedMethods + ) + + override fun visit(version: Int, access: Int, clsName: String, signature: String?, superName: String?, interfaces: Array?) { + _className = clsName + logger.info("Class {}", clsName) + super.visit(version, access, clsName, signature, superName, interfaces) + } + + override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor? { + if (removeAnnotations.contains(descriptor)) { + logger.info("- Removing annotation {}", descriptor) + return null + } else if (deleteAnnotations.contains(descriptor)) { + if (unwantedClasses.add(className)) { + logger.info("- Identified class {} as unwanted", className) + } + } + return super.visitAnnotation(descriptor, visible) + } + + override fun visitField(access: Int, fieldName: String, descriptor: String, signature: String?, value: Any?): FieldVisitor? { + val field = FieldElement(fieldName, descriptor) + logger.debug("--- field ---> {}", field) + if (unwantedFields.contains(field)) { + logger.info("- Deleted field {},{}", field.name, field.descriptor) + unwantedFields.remove(field) + return null + } + val fv = super.visitField(access, fieldName, descriptor, signature, value) ?: return null + return UnwantedFieldAdapter(fv, field) + } + + override fun visitMethod(access: Int, methodName: String, descriptor: String, signature: String?, exceptions: Array?): MethodVisitor? { + val method = MethodElement(methodName, descriptor, access) + logger.debug("--- method ---> {}", method) + if (deletedMethods.contains(method)) { + logger.info("- Deleted method {}{}", method.name, method.descriptor) + deletedMethods.remove(method) + return null + } + + /* + * Write the byte-code for the method's prototype, then check whether + * we need to replace the method's body with our "stub" code. + */ + val mv = super.visitMethod(access, methodName, descriptor, signature, exceptions) ?: return null + if (stubbedMethods.contains(method)) { + logger.info("- Stubbed out method {}{}", method.name, method.descriptor) + stubbedMethods.remove(method) + return if (method.isVoidFunction) VoidStubMethodAdapter(mv) else ThrowingStubMethodAdapter(mv) + } + + return UnwantedMethodAdapter(mv, method) + } + + override fun visitInnerClass(clsName: String, outerName: String?, innerName: String?, access: Int) { + logger.debug("--- inner class {} [outer: {}, inner: {}]", clsName, outerName, innerName) + if (isUnwantedClass || hasDeletedSyntheticMethod(clsName)) { + if (unwantedClasses.add(clsName)) { + logger.info("- Deleted inner class {}", clsName) + } + } else if (isUnwantedClass(clsName)) { + logger.info("- Deleted reference to inner class: {}", clsName) + } else { + super.visitInnerClass(clsName, outerName, innerName, access) + } + } + + override fun visitOuterClass(outerName: String, methodName: String?, methodDescriptor: String?) { + logger.debug("--- outer class {} [enclosing method {},{}]", outerName, methodName, methodDescriptor) + if (isUnwantedClass(outerName)) { + logger.info("- Deleted reference to outer class {}", outerName) + } else { + super.visitOuterClass(outerName, methodName, methodDescriptor) + } + } + + override fun visitEnd() { + if (isUnwantedClass) { + /* + * Optimisation: Don't rewrite the Kotlin @Metadata + * annotation if we're going to delete this class. + */ + kotlinMetadata.clear() + } + super.visitEnd() + /* + * Some elements were created based on unreliable information, + * such as Kotlin @Metadata annotations. We cannot rely on + * these actually existing in the bytecode, and so we expire + * them after a fixed number of passes. + */ + deletedMethods.removeIf(MethodElement::isExpired) + unwantedFields.removeIf(FieldElement::isExpired) + } + + /** + * Removes the deleted methods and fields from the Kotlin Class metadata. + */ + override fun transformClassMetadata(d1: List, d2: List): List { + val partitioned = deletedMethods.groupBy(MethodElement::isConstructor) + val prefix = "$className$" + return ClassMetadataTransformer( + logger = logger, + deletedFields = unwantedFields, + deletedFunctions = partitioned[false] ?: emptyList(), + deletedConstructors = partitioned[true] ?: emptyList(), + deletedNestedClasses = unwantedClasses.filter { it.startsWith(prefix) }.map { it.drop(prefix.length) }, + deletedClasses = unwantedClasses, + handleExtraMethod = ::delete, + d1 = d1, + d2 = d2) + .transform() + } + + /** + * Removes the deleted methods and fields from the Kotlin Package metadata. + */ + override fun transformPackageMetadata(d1: List, d2: List): List { + return PackageMetadataTransformer( + logger = logger, + deletedFields = unwantedFields, + deletedFunctions = deletedMethods, + handleExtraMethod = ::delete, + d1 = d1, + d2 = d2) + .transform() + } + + /** + * Callback function to mark extra methods for deletion. + * This will override a request for stubbing. + */ + private fun delete(method: MethodElement) { + if (deletedMethods.add(method) && stubbedMethods.remove(method)) { + logger.warn("-- method {}{} will be deleted instead of stubbed out", + method.name, method.descriptor) + } + } + + /** + * Analyses the field to decide whether it should be deleted. + */ + private inner class UnwantedFieldAdapter(fv: FieldVisitor, private val field: FieldElement) : FieldVisitor(api, fv) { + override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor? { + if (removeAnnotations.contains(descriptor)) { + logger.info("- Removing annotation {} from field {},{}", descriptor, field.name, field.descriptor) + return null + } else if (deleteAnnotations.contains(descriptor)) { + if (unwantedFields.add(field)) { + logger.info("- Identified field {},{} as unwanted", field.name, field.descriptor) + } + } + return super.visitAnnotation(descriptor, visible) + } + } + + /** + * Analyses the method to decide whether it should be deleted. + */ + private inner class UnwantedMethodAdapter(mv: MethodVisitor, private val method: MethodElement) : MethodVisitor(api, mv) { + override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor? { + if (removeAnnotations.contains(descriptor)) { + logger.info("- Removing annotation {} from method {}{}", descriptor, method.name, method.descriptor) + return null + } else if (deleteAnnotations.contains(descriptor)) { + if (deletedMethods.add(method)) { + logger.info("- Identified method {}{} for deletion", method.name, method.descriptor) + } + if (method.isKotlinSynthetic("annotations")) { + val extensionType = method.descriptor.extensionType + if (unwantedFields.add(FieldElement(name = method.visibleName, extension = extensionType))) { + logger.info("-- also identified property or typealias {},{} for deletion", method.visibleName, extensionType) + } + } + } else if (stubAnnotations.contains(descriptor) && (method.access and (ACC_ABSTRACT or ACC_SYNTHETIC)) == 0) { + if (stubbedMethods.add(method)) { + logger.info("- Identified method {}{} for stubbing out", method.name, method.descriptor) + } + } + return super.visitAnnotation(descriptor, visible) + } + + override fun visitMethodInsn(opcode: Int, ownerName: String, methodName: String, descriptor: String, isInterface: Boolean) { + if ((isUnwantedClass(ownerName) || (ownerName == className && deletedMethods.contains(MethodElement(methodName, descriptor)))) + && !stubbedMethods.contains(method)) { + if (deletedMethods.add(method)) { + logger.info("- Unwanted invocation of method {},{}{} from method {}{}", ownerName, methodName, descriptor, method.name, method.descriptor) + } + } + super.visitMethodInsn(opcode, ownerName, methodName, descriptor, isInterface) + } + + override fun visitFieldInsn(opcode: Int, ownerName: String, fieldName: String, descriptor: String) { + if ((isUnwantedClass(ownerName) || (ownerName == className && unwantedFields.contains(FieldElement(fieldName, descriptor)))) + && !stubbedMethods.contains(method)) { + if (method.isConstructor) { + when (opcode) { + GETFIELD, GETSTATIC -> { + when (descriptor) { + "I", "S", "B", "C", "Z" -> visitIntInsn(BIPUSH, 0) + "J" -> visitInsn(LCONST_0) + "F" -> visitInsn(FCONST_0) + "D" -> visitInsn(DCONST_0) + else -> visitInsn(ACONST_NULL) + } + } + PUTFIELD, PUTSTATIC -> { + when (descriptor) { + "J", "D" -> visitInsn(POP2) + else -> visitInsn(POP) + } + } + else -> throw InvalidUserDataException("Unexpected opcode $opcode") + } + logger.info("- Unwanted reference to field {},{},{} REMOVED from constructor {}{}", + ownerName, fieldName, descriptor, method.name, method.descriptor) + return + } else if (deletedMethods.add(method)) { + logger.info("- Unwanted reference to field {},{},{} from method {}{}", + ownerName, fieldName, descriptor, method.name, method.descriptor) + } + } + super.visitFieldInsn(opcode, ownerName, fieldName, descriptor) + } + } + + /** + * Write "stub" byte-code for this method, preserving its other annotations. + * The method's original byte-code is discarded. + */ + private abstract inner class StubbingMethodAdapter(mv: MethodVisitor) : MethodVisitor(api, mv) { + override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor? { + return if (stubAnnotations.contains(descriptor)) null else mv.visitAnnotation(descriptor, visible) + } + + protected abstract fun writeStubCode() + + final override fun visitCode() { + with (mv) { + visitCode() + writeStubCode() + visitMaxs(-1, -1) // Trigger computation of the max values. + visitEnd() + } + + // Prevent this visitor from writing any more byte-code. + mv = null + } + } + + /** + * Write a method that throws [UnsupportedOperationException] with message "Method has been deleted". + */ + private inner class ThrowingStubMethodAdapter(mv: MethodVisitor) : StubbingMethodAdapter(mv) { + override fun writeStubCode() { + with (mv) { + val throwEx = Label() + visitLabel(throwEx) + visitLineNumber(0, throwEx) + visitTypeInsn(NEW, "java/lang/UnsupportedOperationException") + visitInsn(DUP) + visitLdcInsn("Method has been deleted") + visitMethodInsn(INVOKESPECIAL, "java/lang/UnsupportedOperationException", "", "(Ljava/lang/String;)V", false) + visitInsn(ATHROW) + } + } + } + + /** + * Write an empty method. Can only be applied to methods that return void. + */ + private inner class VoidStubMethodAdapter(mv: MethodVisitor) : StubbingMethodAdapter(mv) { + override fun writeStubCode() { + mv.visitInsn(RETURN) + } + } +} diff --git a/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/Elements.kt b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/Elements.kt new file mode 100644 index 0000000000..54ca3ad542 --- /dev/null +++ b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/Elements.kt @@ -0,0 +1,110 @@ +@file:JvmName("Elements") +package net.corda.gradle.jarfilter + +import org.jetbrains.kotlin.metadata.ProtoBuf +import org.jetbrains.kotlin.metadata.deserialization.NameResolver +import org.jetbrains.kotlin.metadata.deserialization.TypeTable +import org.jetbrains.kotlin.metadata.deserialization.returnType +import org.jetbrains.kotlin.metadata.jvm.JvmProtoBuf +import org.jetbrains.kotlin.metadata.jvm.deserialization.ClassMapperLite +import org.objectweb.asm.Opcodes.ACC_SYNTHETIC +import java.util.* + +private const val DUMMY_PASSES = 1 + +internal abstract class Element(val name: String, val descriptor: String) { + private var lifetime: Int = DUMMY_PASSES + + open val isExpired: Boolean get() = --lifetime < 0 +} + + +internal class MethodElement(name: String, descriptor: String, val access: Int = 0) : Element(name, descriptor) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other?.javaClass != javaClass) return false + other as MethodElement + return other.name == name && other.descriptor == descriptor + } + override fun hashCode(): Int = Objects.hash(name, descriptor) + override fun toString(): String = "MethodElement[name=$name, descriptor=$descriptor, access=$access]" + override val isExpired: Boolean get() = access == 0 && super.isExpired + val isConstructor: Boolean get() = isObjectConstructor || isClassConstructor + val isClassConstructor: Boolean get() = name == "" + val isObjectConstructor: Boolean get() = name == "" + val isVoidFunction: Boolean get() = !isConstructor && descriptor.endsWith(")V") + + private val suffix: String + val visibleName: String + + init { + val idx = name.indexOf('$') + visibleName = if (idx == -1) name else name.substring(0, idx) + suffix = if (idx == -1) "" else name.drop(idx + 1) + } + + fun isKotlinSynthetic(vararg tags: String): Boolean = (access and ACC_SYNTHETIC) != 0 && tags.contains(suffix) +} + + +/** + * A class cannot have two fields with the same name but different types. However, + * it can define extension functions and properties. + */ +internal class FieldElement(name: String, descriptor: String = "?", val extension: String = "()") : Element(name, descriptor) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other?.javaClass != javaClass) return false + other as FieldElement + return other.name == name && other.extension == extension + } + override fun hashCode(): Int = Objects.hash(name, extension) + override fun toString(): String = "FieldElement[name=$name, descriptor=$descriptor, extension=$extension]" + override val isExpired: Boolean get() = descriptor == "?" && super.isExpired +} + +val String.extensionType: String get() = substring(0, 1 + indexOf(')')) + +/** + * Convert Kotlin getter/setter method data to [MethodElement] objects. + */ +internal fun JvmProtoBuf.JvmPropertySignature.toGetter(nameResolver: NameResolver): MethodElement? { + return if (hasGetter()) { getter?.toMethodElement(nameResolver) } else { null } +} + +internal fun JvmProtoBuf.JvmPropertySignature.toSetter(nameResolver: NameResolver): MethodElement? { + return if (hasSetter()) { setter?.toMethodElement(nameResolver) } else { null } +} + +internal fun JvmProtoBuf.JvmMethodSignature.toMethodElement(nameResolver: NameResolver) + = MethodElement(nameResolver.getString(name), nameResolver.getString(desc)) + +/** + * This logic is based heavily on [JvmProtoBufUtil.getJvmFieldSignature]. + */ +internal fun JvmProtoBuf.JvmPropertySignature.toFieldElement(property: ProtoBuf.Property, nameResolver: NameResolver, typeTable: TypeTable): FieldElement { + var nameId = property.name + var descId = -1 + + if (hasField()) { + if (field.hasName()) { + nameId = field.name + } + if (field.hasDesc()) { + descId = field.desc + } + } + + val descriptor = if (descId == -1) { + val returnType = property.returnType(typeTable) + if (returnType.hasClassName()) { + ClassMapperLite.mapClass(nameResolver.getQualifiedClassName(returnType.className)) + } else { + "?" + } + } else { + nameResolver.getString(descId) + } + + return FieldElement(nameResolver.getString(nameId), descriptor) +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/JarFilterPlugin.kt b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/JarFilterPlugin.kt new file mode 100644 index 0000000000..ae33419b2b --- /dev/null +++ b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/JarFilterPlugin.kt @@ -0,0 +1,14 @@ +@file:Suppress("UNUSED") +package net.corda.gradle.jarfilter + +import org.gradle.api.Plugin +import org.gradle.api.Project + +/** + * This plugin definition is only needed by the tests. + */ +class JarFilterPlugin : Plugin { + override fun apply(project: Project) { + project.logger.info("Applying JarFilter plugin") + } +} diff --git a/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/JarFilterTask.kt b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/JarFilterTask.kt new file mode 100644 index 0000000000..7d7b87e264 --- /dev/null +++ b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/JarFilterTask.kt @@ -0,0 +1,254 @@ +package net.corda.gradle.jarfilter + +import groovy.lang.Closure +import org.gradle.api.DefaultTask +import org.gradle.api.InvalidUserDataException +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.FileCollection +import org.gradle.api.tasks.* +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.ClassWriter.COMPUTE_MAXS +import java.io.Closeable +import java.io.File +import java.io.IOException +import java.nio.file.* +import java.nio.file.StandardCopyOption.* +import java.util.zip.Deflater.BEST_COMPRESSION +import java.util.zip.ZipFile +import java.util.zip.ZipOutputStream +import kotlin.math.max + +@Suppress("Unused", "MemberVisibilityCanBePrivate") +open class JarFilterTask : DefaultTask() { + private companion object { + private const val DEFAULT_MAX_PASSES = 5 + } + + private val _jars: ConfigurableFileCollection = project.files() + @get:SkipWhenEmpty + @get:InputFiles + val jars: FileCollection get() = _jars + + fun setJars(inputs: Any?) { + val files = inputs ?: return + _jars.setFrom(files) + } + + fun jars(inputs: Any?) = setJars(inputs) + + @get:Input + protected var forDelete: Set = emptySet() + + @get:Input + protected var forStub: Set = emptySet() + + @get:Input + protected var forRemove: Set = emptySet() + + fun annotations(assign: Closure>) { + assign.call() + } + + @get:Console + var verbose: Boolean = false + + @get:Input + var maxPasses: Int = DEFAULT_MAX_PASSES + set(value) { + field = max(value, 1) + } + + @get:Input + var preserveTimestamps: Boolean = true + + private var _outputDir = project.buildDir.resolve("filtered-libs") + @get:Internal + val outputDir: File get() = _outputDir + + fun setOutputDir(d: File?) { + val dir = d ?: return + _outputDir = dir + } + + fun outputDir(dir: File?) = setOutputDir(dir) + + @get:OutputFiles + val filtered: FileCollection get() = project.files(jars.files.map(this::toFiltered)) + + private fun toFiltered(source: File) = File(outputDir, source.name.replace(JAR_PATTERN, "-filtered\$1")) + + @TaskAction + fun filterJars() { + logger.info("JarFiltering:") + if (forDelete.isNotEmpty()) { + logger.info("- Elements annotated with one of '{}' will be deleted", forDelete.joinToString()) + } + if (forStub.isNotEmpty()) { + logger.info("- Methods annotated with one of '{}' will be stubbed out", forStub.joinToString()) + } + if (forRemove.isNotEmpty()) { + logger.info("- Annotations '{}' will be removed entirely", forRemove.joinToString()) + } + checkDistinctAnnotations() + try { + jars.forEach { jar -> + logger.info("Filtering {}", jar) + Filter(jar).run() + } + } catch (e: Exception) { + rethrowAsUncheckedException(e) + } + } + + private fun checkDistinctAnnotations() { + logger.info("Checking that all annotations are distinct.") + val annotations = forRemove.toHashSet().apply { + addAll(forDelete) + addAll(forStub) + removeAll(forRemove) + } + forDelete.forEach { + if (!annotations.remove(it)) { + failWith("Annotation '$it' also appears in JarFilter 'forDelete' section") + } + } + forStub.forEach { + if (!annotations.remove(it)) { + failWith("Annotation '$it' also appears in JarFilter 'forStub' section") + } + } + if (!annotations.isEmpty()) { + failWith("SHOULDN'T HAPPEN - Martian annotations! '${annotations.joinToString()}'") + } + } + + private fun failWith(message: String): Nothing = throw InvalidUserDataException(message) + + private fun verbose(format: String, vararg objects: Any) { + if (verbose) { + logger.info(format, *objects) + } + } + + private inner class Filter(inFile: File) { + private val unwantedClasses: MutableSet = mutableSetOf() + private val source: Path = inFile.toPath() + private val target: Path = toFiltered(inFile).toPath() + + init { + Files.deleteIfExists(target) + } + + fun run() { + logger.info("Filtering to: {}", target) + var input = source + + try { + var passes = 1 + while (true) { + verbose("Pass {}", passes) + val isModified = Pass(input).use { it.run() } + + if (!isModified) { + logger.info("No changes after latest pass - exiting.") + break + } else if (++passes > maxPasses) { + break + } + + input = Files.move( + target, Files.createTempFile(target.parent, "filter-", ".tmp"), REPLACE_EXISTING) + verbose("New input JAR: {}", input) + } + } catch (e: Exception) { + logger.error("Error filtering '{}' elements from {}", ArrayList(forRemove).apply { addAll(forDelete); addAll(forStub) }, input) + throw e + } + } + + private inner class Pass(input: Path): Closeable { + /** + * Use [ZipFile] instead of [java.util.jar.JarInputStream] because + * JarInputStream consumes MANIFEST.MF when it's the first or second entry. + */ + private val inJar = ZipFile(input.toFile()) + private val outJar = ZipOutputStream(Files.newOutputStream(target)) + private var isModified = false + + @Throws(IOException::class) + override fun close() { + inJar.use { + outJar.close() + } + } + + fun run(): Boolean { + outJar.setLevel(BEST_COMPRESSION) + outJar.setComment(inJar.comment) + + for (entry in inJar.entries()) { + val entryData = inJar.getInputStream(entry) + + if (entry.isDirectory || !entry.name.endsWith(".class")) { + // This entry's byte contents have not changed, + // but may still need to be recompressed. + outJar.putNextEntry(entry.copy().withFileTimestamps(preserveTimestamps)) + entryData.copyTo(outJar) + } else { + val classData = transform(entryData.readBytes()) + if (classData.isNotEmpty()) { + // This entry's byte contents have almost certainly + // changed, and will be stored compressed. + outJar.putNextEntry(entry.asCompressed().withFileTimestamps(preserveTimestamps)) + outJar.write(classData) + } + } + } + return isModified + } + + private fun transform(inBytes: ByteArray): ByteArray { + var reader = ClassReader(inBytes) + var writer = ClassWriter(COMPUTE_MAXS) + var transformer = ClassTransformer( + visitor = writer, + logger = logger, + removeAnnotations = toDescriptors(forRemove), + deleteAnnotations = toDescriptors(forDelete), + stubAnnotations = toDescriptors(forStub), + unwantedClasses = unwantedClasses + ) + + /* + * First pass: This might not find anything to remove! + */ + reader.accept(transformer, 0) + + if (transformer.isUnwantedClass || transformer.hasUnwantedElements) { + isModified = true + + do { + /* + * Rewrite the class without any of the unwanted elements. + * If we're deleting the class then make sure we identify all of + * its inner classes too, for the next filter pass to delete. + */ + reader = ClassReader(writer.toByteArray()) + writer = ClassWriter(COMPUTE_MAXS) + transformer = transformer.recreate(writer) + reader.accept(transformer, 0) + } while (!transformer.isUnwantedClass && transformer.hasUnwantedElements) + } + + return if (transformer.isUnwantedClass) { + // The entire class is unwanted, so don't write it out. + logger.info("Deleting class {}", transformer.className) + byteArrayOf() + } else { + writer.toByteArray() + } + } + } + } +} diff --git a/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/KotlinAwareVisitor.kt b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/KotlinAwareVisitor.kt new file mode 100644 index 0000000000..ae078c69b4 --- /dev/null +++ b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/KotlinAwareVisitor.kt @@ -0,0 +1,109 @@ +package net.corda.gradle.jarfilter + +import org.gradle.api.logging.Logger +import org.jetbrains.kotlin.load.java.JvmAnnotationNames.* +import org.objectweb.asm.AnnotationVisitor +import org.objectweb.asm.ClassVisitor + +/** + * Kotlin support: Loads the ProtoBuf data from the [kotlin.Metadata] annotation, + * or writes new ProtoBuf data that was created during a previous pass. + */ +abstract class KotlinAwareVisitor( + api: Int, + visitor: ClassVisitor, + protected val logger: Logger, + protected val kotlinMetadata: MutableMap> +) : ClassVisitor(api, visitor) { + + private companion object { + /** See [org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader.Kind]. */ + private const val KOTLIN_CLASS = 1 + private const val KOTLIN_FILE = 2 + private const val KOTLIN_SYNTHETIC = 3 + private const val KOTLIN_MULTIFILE_PART = 5 + } + + private var classKind: Int = 0 + + open val hasUnwantedElements: Boolean get() = kotlinMetadata.isNotEmpty() + + protected abstract fun transformClassMetadata(d1: List, d2: List): List + protected abstract fun transformPackageMetadata(d1: List, d2: List): List + + override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor? { + val av = super.visitAnnotation(descriptor, visible) ?: return null + return if (descriptor == METADATA_DESC) KotlinMetadataAdaptor(av) else av + } + + override fun visitEnd() { + super.visitEnd() + if (kotlinMetadata.isNotEmpty()) { + logger.info("- Examining Kotlin @Metadata[k={}]", classKind) + val d1 = kotlinMetadata.remove(METADATA_DATA_FIELD_NAME) + val d2 = kotlinMetadata.remove(METADATA_STRINGS_FIELD_NAME) + if (d1 != null && d1.isNotEmpty() && d2 != null) { + transformMetadata(d1, d2).apply { + if (isNotEmpty()) { + kotlinMetadata[METADATA_DATA_FIELD_NAME] = this + kotlinMetadata[METADATA_STRINGS_FIELD_NAME] = d2 + } + } + } + } + } + + private fun transformMetadata(d1: List, d2: List): List { + return when (classKind) { + KOTLIN_CLASS -> transformClassMetadata(d1, d2) + KOTLIN_FILE, KOTLIN_MULTIFILE_PART -> transformPackageMetadata(d1, d2) + KOTLIN_SYNTHETIC -> { + logger.info("-- synthetic class ignored") + emptyList() + } + else -> { + /* + * For class-kind=4 (i.e. "multi-file"), we currently + * expect d1=[list of multi-file-part classes], d2=null. + */ + logger.info("-- unsupported class-kind {}", classKind) + emptyList() + } + } + } + + private inner class KotlinMetadataAdaptor(av: AnnotationVisitor): AnnotationVisitor(api, av) { + override fun visit(name: String?, value: Any?) { + if (name == KIND_FIELD_NAME) { + classKind = value as Int + } + super.visit(name, value) + } + + override fun visitArray(name: String): AnnotationVisitor? { + val av = super.visitArray(name) + if (av != null) { + val data = kotlinMetadata.remove(name) ?: return ArrayAccumulator(av, name) + logger.debug("-- rewrote @Metadata.{}[{}]", name, data.size) + data.forEach { av.visit(null, it) } + av.visitEnd() + } + return null + } + + private inner class ArrayAccumulator(av: AnnotationVisitor, private val name: String) : AnnotationVisitor(api, av) { + private val data: MutableList = mutableListOf() + + override fun visit(name: String?, value: Any?) { + super.visit(name, value) + data.add(value as String) + } + + override fun visitEnd() { + super.visitEnd() + kotlinMetadata[name] = data + logger.debug("-- read @Metadata.{}[{}]", name, data.size) + } + } + } +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetaFixerTask.kt b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetaFixerTask.kt new file mode 100644 index 0000000000..5c120cc926 --- /dev/null +++ b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetaFixerTask.kt @@ -0,0 +1,128 @@ +package net.corda.gradle.jarfilter + +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.FileCollection +import org.gradle.api.logging.Logger +import org.gradle.api.tasks.* +import java.io.Closeable +import java.io.File +import java.io.IOException +import java.nio.file.* +import java.util.zip.Deflater.BEST_COMPRESSION +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import java.util.zip.ZipOutputStream + +@Suppress("Unused", "MemberVisibilityCanBePrivate") +open class MetaFixerTask : DefaultTask() { + private val _jars: ConfigurableFileCollection = project.files() + @get:SkipWhenEmpty + @get:InputFiles + val jars: FileCollection + get() = _jars + + fun setJars(inputs: Any?) { + val files = inputs ?: return + _jars.setFrom(files) + } + + fun jars(inputs: Any?) = setJars(inputs) + + private var _outputDir = project.buildDir.resolve("metafixer-libs") + @get:Internal + val outputDir: File + get() = _outputDir + + fun setOutputDir(d: File?) { + val dir = d ?: return + _outputDir = dir + } + + fun outputDir(dir: File?) = setOutputDir(dir) + + private var _suffix: String = "-metafixed" + @get:Input + val suffix: String get() = _suffix + + fun setSuffix(input: String?) { + _suffix = input ?: return + } + + fun suffix(suffix: String?) = setSuffix(suffix) + + @get:Input + var preserveTimestamps: Boolean = true + + @TaskAction + fun fixMetadata() { + logger.info("Fixing Kotlin @Metadata") + try { + jars.forEach { jar -> + logger.info("Reading from {}", jar) + MetaFix(jar).use { it.run() } + } + } catch (e: Exception) { + rethrowAsUncheckedException(e) + } + } + + @get:OutputFiles + val metafixed: FileCollection get() = project.files(jars.files.map(this::toMetaFixed)) + + private fun toMetaFixed(source: File) = File(outputDir, source.name.replace(JAR_PATTERN, "$suffix\$1")) + + private inner class MetaFix(inFile: File) : Closeable { + /** + * Use [ZipFile] instead of [java.util.jar.JarInputStream] because + * JarInputStream consumes MANIFEST.MF when it's the first or second entry. + */ + private val target: Path = toMetaFixed(inFile).toPath() + private val inJar = ZipFile(inFile) + private val outJar: ZipOutputStream + + init { + // Default options for newOutputStream() are CREATE, TRUNCATE_EXISTING. + outJar = ZipOutputStream(Files.newOutputStream(target)).apply { + setLevel(BEST_COMPRESSION) + } + } + + @Throws(IOException::class) + override fun close() { + inJar.use { + outJar.close() + } + } + + fun run() { + logger.info("Writing to {}", target) + outJar.setComment(inJar.comment) + + val classNames = inJar.entries().asSequence().namesEndingWith(".class") + for (entry in inJar.entries()) { + val entryData = inJar.getInputStream(entry) + + if (entry.isDirectory || !entry.name.endsWith(".class")) { + // This entry's byte contents have not changed, + // but may still need to be recompressed. + outJar.putNextEntry(entry.copy().withFileTimestamps(preserveTimestamps)) + entryData.copyTo(outJar) + } else { + // This entry's byte contents have almost certainly + // changed, and will be stored compressed. + val classData = entryData.readBytes().fixMetadata(logger, classNames) + outJar.putNextEntry(entry.asCompressed().withFileTimestamps(preserveTimestamps)) + outJar.write(classData) + } + } + } + } + + private fun Sequence.namesEndingWith(suffix: String): Set { + return filter { it.name.endsWith(suffix) }.map { it.name.dropLast(suffix.length) }.toSet() + } +} + +fun ByteArray.fixMetadata(logger: Logger, classNames: Set): ByteArray + = execute({ writer -> MetaFixerVisitor(writer, logger, classNames) }) diff --git a/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetaFixerTransformer.kt b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetaFixerTransformer.kt new file mode 100644 index 0000000000..65988db056 --- /dev/null +++ b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetaFixerTransformer.kt @@ -0,0 +1,258 @@ +package net.corda.gradle.jarfilter + +import org.gradle.api.logging.Logger +import org.jetbrains.kotlin.metadata.ProtoBuf +import org.jetbrains.kotlin.metadata.ProtoBuf.Class.Kind.* +import org.jetbrains.kotlin.metadata.deserialization.Flags.* +import org.jetbrains.kotlin.metadata.deserialization.NameResolver +import org.jetbrains.kotlin.metadata.deserialization.TypeTable +import org.jetbrains.kotlin.metadata.deserialization.getExtensionOrNull +import org.jetbrains.kotlin.metadata.jvm.JvmProtoBuf.* +import org.jetbrains.kotlin.metadata.jvm.deserialization.BitEncoding +import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmNameResolver +import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil +import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil.EXTENSION_REGISTRY +import org.jetbrains.kotlin.protobuf.ExtensionRegistryLite +import org.jetbrains.kotlin.protobuf.MessageLite +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream + +/** + * Base class for aligning the contents of [kotlin.Metadata] annotations + * with the contents of the host byte-code. + * This is used by [MetaFixerVisitor] for [MetaFixerTask]. + */ +internal abstract class MetaFixerTransformer( + private val logger: Logger, + private val actualFields: Collection, + private val actualMethods: Collection, + private val actualNestedClasses: Collection, + private val actualClasses: Collection, + d1: List, + d2: List, + parser: (InputStream, ExtensionRegistryLite) -> T +) { + private val stringTableTypes: StringTableTypes + private val nameResolver: NameResolver + protected val message: T + + protected abstract val typeTable: TypeTable + protected open val classKind: ProtoBuf.Class.Kind? = null + protected abstract val properties: MutableList + protected abstract val functions: MutableList + protected abstract val constructors: MutableList + protected open val nestedClassNames: MutableList get() = throw UnsupportedOperationException("No nestedClassNames") + protected open val sealedSubclassNames: MutableList get() = throw UnsupportedOperationException("No sealedSubclassNames") + + init { + val input = ByteArrayInputStream(BitEncoding.decodeBytes(d1.toTypedArray())) + stringTableTypes = StringTableTypes.parseDelimitedFrom(input, EXTENSION_REGISTRY) + nameResolver = JvmNameResolver(stringTableTypes, d2.toTypedArray()) + message = parser(input, EXTENSION_REGISTRY) + } + + abstract fun rebuild(): T + + private fun filterNestedClasses(): Int { + if (classKind == null) return 0 + + var count = 0 + var idx = 0 + while (idx < nestedClassNames.size) { + val nestedClassName = nameResolver.getString(nestedClassNames[idx]) + if (actualNestedClasses.contains(nestedClassName)) { + ++idx + } else { + logger.info("-- removing nested class: {}", nestedClassName) + nestedClassNames.removeAt(idx) + ++count + } + } + return count + } + + private fun filterSealedSubclassNames(): Int { + if (classKind == null) return 0 + + var count = 0 + var idx = 0 + while (idx < sealedSubclassNames.size) { + val sealedSubclassName = nameResolver.getString(sealedSubclassNames[idx]).replace('.', '$') + if (actualClasses.contains(sealedSubclassName)) { + ++idx + } else { + logger.info("-- removing sealed subclass: {}", sealedSubclassName) + sealedSubclassNames.removeAt(idx) + ++count + } + } + return count + } + + private fun filterFunctions(): Int { + var count = 0 + var idx = 0 + while (idx < functions.size) { + val signature = JvmProtoBufUtil.getJvmMethodSignature(functions[idx], nameResolver, typeTable) + if ((signature == null) || actualMethods.contains(signature)) { + ++idx + } else { + logger.info("-- removing method: {}", signature) + functions.removeAt(idx) + ++count + } + } + return count + } + + private fun filterConstructors(): Int { + var count = 0 + var idx = 0 + while (idx < constructors.size) { + val signature = JvmProtoBufUtil.getJvmConstructorSignature(constructors[idx], nameResolver, typeTable) + if ((signature == null) || actualMethods.contains(signature)) { + ++idx + } else { + logger.info("-- removing constructor: {}", signature) + constructors.removeAt(idx) + ++count + } + } + return count + } + + private fun filterProperties(): Int { + var count = 0 + var idx = 0 + removed@ while (idx < properties.size) { + val property = properties[idx] + val signature = property.getExtensionOrNull(propertySignature) ?: continue + val field = signature.toFieldElement(property, nameResolver, typeTable) + val getterMethod = signature.toGetter(nameResolver) + + /** + * A property annotated with [JvmField] will use a field instead of a getter method. + * But properties without [JvmField] will also usually have a backing field. So we only + * remove a property that has either lost its getter method, or never had a getter method + * and has lost its field. + * + * Having said that, we cannot remove [JvmField] properties from a companion object class + * because these properties are implemented as static fields on the companion's host class. + */ + val isValidProperty = if (getterMethod == null) { + actualFields.contains(field) || classKind == COMPANION_OBJECT + } else { + actualMethods.contains(getterMethod.name + getterMethod.descriptor) + } + + if (!isValidProperty) { + logger.info("-- removing property: {},{}", field.name, field.descriptor) + properties.removeAt(idx) + ++count + continue@removed + } + ++idx + } + return count + } + + fun transform(): List { + var count = filterProperties() + filterFunctions() + filterNestedClasses() + filterSealedSubclassNames() + if (classKind != ANNOTATION_CLASS) { + count += filterConstructors() + } + if (count == 0) { + return emptyList() + } + + val bytes = ByteArrayOutputStream() + stringTableTypes.writeDelimitedTo(bytes) + rebuild().writeTo(bytes) + return BitEncoding.encodeBytes(bytes.toByteArray()).toList() + } +} + +/** + * Aligns a [kotlin.Metadata] annotation containing a [ProtoBuf.Class] object + * in its [d1][kotlin.Metadata.d1] field with the byte-code of its host class. + */ +internal class ClassMetaFixerTransformer( + logger: Logger, + actualFields: Collection, + actualMethods: Collection, + actualNestedClasses: Collection, + actualClasses: Collection, + d1: List, + d2: List +) : MetaFixerTransformer( + logger, + actualFields, + actualMethods, + actualNestedClasses, + actualClasses, + d1, + d2, + ProtoBuf.Class::parseFrom +) { + override val typeTable = TypeTable(message.typeTable) + override val classKind: ProtoBuf.Class.Kind = CLASS_KIND.get(message.flags) + override val properties = mutableList(message.propertyList) + override val functions = mutableList(message.functionList) + override val constructors = mutableList(message.constructorList) + override val nestedClassNames = mutableList(message.nestedClassNameList) + override val sealedSubclassNames= mutableList(message.sealedSubclassFqNameList) + + override fun rebuild(): ProtoBuf.Class = message.toBuilder().apply { + if (nestedClassNames.size != nestedClassNameCount) { + clearNestedClassName().addAllNestedClassName(nestedClassNames) + } + if (sealedSubclassNames.size != sealedSubclassFqNameCount) { + clearSealedSubclassFqName().addAllSealedSubclassFqName(sealedSubclassNames) + } + if (constructors.size != constructorCount) { + clearConstructor().addAllConstructor(constructors) + } + if (functions.size != functionCount) { + clearFunction().addAllFunction(functions) + } + if (properties.size != propertyCount) { + clearProperty().addAllProperty(properties) + } + }.build() +} + +/** + * Aligns a [kotlin.Metadata] annotation containing a [ProtoBuf.Package] object + * in its [d1][kotlin.Metadata.d1] field with the byte-code of its host class. + */ +internal class PackageMetaFixerTransformer( + logger: Logger, + actualFields: Collection, + actualMethods: Collection, + d1: List, + d2: List +) : MetaFixerTransformer( + logger, + actualFields, + actualMethods, + emptyList(), + emptyList(), + d1, + d2, + ProtoBuf.Package::parseFrom +) { + override val typeTable = TypeTable(message.typeTable) + override val properties = mutableList(message.propertyList) + override val functions = mutableList(message.functionList) + override val constructors = mutableListOf() + + override fun rebuild(): ProtoBuf.Package = message.toBuilder().apply { + if (functions.size != functionCount) { + clearFunction().addAllFunction(functions) + } + if (properties.size != propertyCount) { + clearProperty().addAllProperty(properties) + } + }.build() +} diff --git a/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetaFixerVisitor.kt b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetaFixerVisitor.kt new file mode 100644 index 0000000000..a122cf4308 --- /dev/null +++ b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetaFixerVisitor.kt @@ -0,0 +1,76 @@ +package net.corda.gradle.jarfilter + +import org.gradle.api.logging.Logger +import org.objectweb.asm.* +import org.objectweb.asm.Opcodes.* + +/** + * ASM [ClassVisitor] for the MetaFixer task. This visitor inventories every function, + * property and inner class within the byte-code and then passes this information to + * the [MetaFixerTransformer]. + */ +class MetaFixerVisitor private constructor( + visitor: ClassVisitor, + logger: Logger, + kotlinMetadata: MutableMap>, + private val classNames: Set, + private val fields: MutableSet, + private val methods: MutableSet, + private val nestedClasses: MutableSet +) : KotlinAwareVisitor(ASM6, visitor, logger, kotlinMetadata), Repeatable { + constructor(visitor: ClassVisitor, logger: Logger, classNames: Set) + : this(visitor, logger, mutableMapOf(), classNames, mutableSetOf(), mutableSetOf(), mutableSetOf()) + + override fun recreate(visitor: ClassVisitor) = MetaFixerVisitor(visitor, logger, kotlinMetadata, classNames, fields, methods, nestedClasses) + + private var className: String = "(unknown)" + + override fun visit(version: Int, access: Int, clsName: String, signature: String?, superName: String?, interfaces: Array?) { + className = clsName + logger.info("Class {}", clsName) + super.visit(version, access, clsName, signature, superName, interfaces) + } + + override fun visitField(access: Int, fieldName: String, descriptor: String, signature: String?, value: Any?): FieldVisitor? { + if (fields.add(FieldElement(fieldName, descriptor))) { + logger.info("- field {},{}", fieldName, descriptor) + } + return super.visitField(access, fieldName, descriptor, signature, value) + } + + override fun visitMethod(access: Int, methodName: String, descriptor: String, signature: String?, exceptions: Array?): MethodVisitor? { + if (methods.add(methodName + descriptor)) { + logger.info("- method {}{}", methodName, descriptor) + } + return super.visitMethod(access, methodName, descriptor, signature, exceptions) + } + + override fun visitInnerClass(clsName: String, outerName: String?, innerName: String?, access: Int) { + if (outerName == className && innerName != null && nestedClasses.add(innerName)) { + logger.info("- inner class {}", clsName) + } + return super.visitInnerClass(clsName, outerName, innerName, access) + } + + override fun transformClassMetadata(d1: List, d2: List): List { + return ClassMetaFixerTransformer( + logger = logger, + actualFields = fields, + actualMethods = methods, + actualNestedClasses = nestedClasses, + actualClasses = classNames, + d1 = d1, + d2 = d2) + .transform() + } + + override fun transformPackageMetadata(d1: List, d2: List): List { + return PackageMetaFixerTransformer( + logger = logger, + actualFields = fields, + actualMethods = methods, + d1 = d1, + d2 = d2) + .transform() + } +} diff --git a/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetadataTransformer.kt b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetadataTransformer.kt new file mode 100644 index 0000000000..6c75fe0121 --- /dev/null +++ b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetadataTransformer.kt @@ -0,0 +1,307 @@ +package net.corda.gradle.jarfilter + +import org.gradle.api.logging.Logger +import org.jetbrains.kotlin.metadata.ProtoBuf +import org.jetbrains.kotlin.metadata.deserialization.Flags.* +import org.jetbrains.kotlin.metadata.deserialization.NameResolver +import org.jetbrains.kotlin.metadata.deserialization.TypeTable +import org.jetbrains.kotlin.metadata.deserialization.getExtensionOrNull +import org.jetbrains.kotlin.metadata.jvm.JvmProtoBuf.* +import org.jetbrains.kotlin.metadata.jvm.deserialization.BitEncoding +import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmNameResolver +import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil +import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil.EXTENSION_REGISTRY +import org.jetbrains.kotlin.protobuf.ExtensionRegistryLite +import org.jetbrains.kotlin.protobuf.MessageLite +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream + +/** + * Base class for removing unwanted elements from [kotlin.Metadata] annotations. + * This is used by [ClassTransformer] for [JarFilterTask]. + */ +internal abstract class MetadataTransformer( + private val logger: Logger, + private val deletedFields: Collection, + private val deletedFunctions: Collection, + private val deletedConstructors: Collection, + private val deletedNestedClasses: Collection, + private val deletedClasses: Collection, + private val handleExtraMethod: (MethodElement) -> Unit, + d1: List, + d2: List, + parser: (InputStream, ExtensionRegistryLite) -> T +) { + private val stringTableTypes: StringTableTypes + protected val nameResolver: NameResolver + protected val message: T + + protected abstract val typeTable: TypeTable + protected open val className: String get() = throw UnsupportedOperationException("No className") + protected open val nestedClassNames: MutableList get() = throw UnsupportedOperationException("No nestedClassNames") + protected open val sealedSubclassNames: MutableList get() = throw UnsupportedOperationException("No sealedSubclassNames") + protected abstract val properties: MutableList + protected abstract val functions: MutableList + protected open val constructors: MutableList get() = throw UnsupportedOperationException("No constructors") + protected abstract val typeAliases: MutableList + + init { + val input = ByteArrayInputStream(BitEncoding.decodeBytes(d1.toTypedArray())) + stringTableTypes = StringTableTypes.parseDelimitedFrom(input, EXTENSION_REGISTRY) + nameResolver = JvmNameResolver(stringTableTypes, d2.toTypedArray()) + message = parser(input, EXTENSION_REGISTRY) + } + + abstract fun rebuild(): T + + fun transform(): List { + val count = ( + filterProperties() + + filterFunctions() + + filterConstructors() + + filterNestedClasses() + + filterTypeAliases() + + filterSealedSubclasses() + ) + if (count == 0) { + return emptyList() + } + + val bytes = ByteArrayOutputStream() + stringTableTypes.writeDelimitedTo(bytes) + rebuild().writeTo(bytes) + return BitEncoding.encodeBytes(bytes.toByteArray()).toList() + } + + private fun filterNestedClasses(): Int { + if (deletedNestedClasses.isEmpty()) return 0 + + var count = 0 + var idx = 0 + while (idx < nestedClassNames.size) { + val nestedClassName = nameResolver.getString(nestedClassNames[idx]) + if (deletedNestedClasses.contains(nestedClassName)) { + logger.info("-- removing nested class: {}", nestedClassName) + nestedClassNames.removeAt(idx) + ++count + } else { + ++idx + } + } + return count + } + + private fun filterConstructors(): Int = deletedConstructors.count(::filterConstructor) + + private fun filterConstructor(deleted: MethodElement): Boolean { + for (idx in 0 until constructors.size) { + val constructor = constructors[idx] + val signature = JvmProtoBufUtil.getJvmConstructorSignature(constructor, nameResolver, typeTable) + if (signature == deleted.name + deleted.descriptor) { + if (IS_SECONDARY.get(constructor.flags)) { + logger.info("-- removing constructor: {}{}", deleted.name, deleted.descriptor) + } else { + logger.warn("Removing primary constructor: {}{}", className, deleted.descriptor) + } + constructors.removeAt(idx) + return true + } + } + return false + } + + private fun filterFunctions(): Int = deletedFunctions.count(::filterFunction) + + private fun filterFunction(deleted: MethodElement): Boolean { + for (idx in 0 until functions.size) { + val function = functions[idx] + if (nameResolver.getString(function.name) == deleted.name) { + val signature = JvmProtoBufUtil.getJvmMethodSignature(function, nameResolver, typeTable) + if (signature == deleted.name + deleted.descriptor) { + logger.info("-- removing function: {}{}", deleted.name, deleted.descriptor) + functions.removeAt(idx) + return true + } + } + } + return false + } + + private fun filterProperties(): Int = deletedFields.count(::filterProperty) + + private fun filterProperty(deleted: FieldElement): Boolean { + for (idx in 0 until properties.size) { + val property = properties[idx] + val signature = property.getExtensionOrNull(propertySignature) ?: continue + val field = signature.toFieldElement(property, nameResolver, typeTable) + if (field.name.toVisible() == deleted.name) { + // Check that this property's getter has the correct descriptor. + // If it doesn't then we have the wrong property here. + val getter = signature.toGetter(nameResolver) + if (getter != null) { + if (!getter.descriptor.startsWith(deleted.extension)) { + continue + } + deleteExtra(getter) + } + signature.toSetter(nameResolver)?.apply(::deleteExtra) + + logger.info("-- removing property: {},{}", field.name, field.descriptor) + properties.removeAt(idx) + return true + } + } + return false + } + + private fun deleteExtra(func: MethodElement) { + if (!deletedFunctions.contains(func)) { + logger.info("-- identified extra method {}{} for deletion", func.name, func.descriptor) + handleExtraMethod(func) + filterFunction(func) + } + } + + private fun filterTypeAliases(): Int { + if (deletedFields.isEmpty()) return 0 + + var count = 0 + var idx = 0 + while (idx < typeAliases.size) { + val aliasName = nameResolver.getString(typeAliases[idx].name) + if (deletedFields.any { it.name == aliasName && it.extension == "()" }) { + logger.info("-- removing typealias: {}", aliasName) + typeAliases.removeAt(idx) + ++count + } else { + ++idx + } + } + return count + } + + private fun filterSealedSubclasses(): Int { + if (deletedClasses.isEmpty()) return 0 + + var count = 0 + var idx = 0 + while (idx < sealedSubclassNames.size) { + val subclassName = nameResolver.getString(sealedSubclassNames[idx]).replace('.', '$') + if (deletedClasses.contains(subclassName)) { + logger.info("-- removing sealed subclass: {}", subclassName) + sealedSubclassNames.removeAt(idx) + ++count + } else { + ++idx + } + } + return count + } + + /** + * Removes any Kotlin suffix, e.g. "$delegate" or "$annotations". + */ + private fun String.toVisible(): String { + val idx = indexOf('$') + return if (idx == -1) this else substring(0, idx) + } +} + +/** + * Removes elements from a [kotlin.Metadata] annotation that contains + * a [ProtoBuf.Class] object in its [d1][kotlin.Metadata.d1] field. + */ +internal class ClassMetadataTransformer( + logger: Logger, + deletedFields: Collection, + deletedFunctions: Collection, + deletedConstructors: Collection, + deletedNestedClasses: Collection, + deletedClasses: Collection, + handleExtraMethod: (MethodElement) -> Unit, + d1: List, + d2: List +) : MetadataTransformer( + logger, + deletedFields, + deletedFunctions, + deletedConstructors, + deletedNestedClasses, + deletedClasses, + handleExtraMethod, + d1, + d2, + ProtoBuf.Class::parseFrom +) { + override val typeTable = TypeTable(message.typeTable) + override val className = nameResolver.getString(message.fqName) + override val nestedClassNames = mutableList(message.nestedClassNameList) + override val sealedSubclassNames = mutableList(message.sealedSubclassFqNameList) + override val properties = mutableList(message.propertyList) + override val functions = mutableList(message.functionList) + override val constructors = mutableList(message.constructorList) + override val typeAliases = mutableList(message.typeAliasList) + + override fun rebuild(): ProtoBuf.Class = message.toBuilder().apply { + if (nestedClassNames.size != nestedClassNameCount) { + clearNestedClassName().addAllNestedClassName(nestedClassNames) + } + if (constructors.size != constructorCount) { + clearConstructor().addAllConstructor(constructors) + } + if (functions.size != functionCount) { + clearFunction().addAllFunction(functions) + } + if (properties.size != propertyCount) { + clearProperty().addAllProperty(properties) + } + if (typeAliases.size != typeAliasCount) { + clearTypeAlias().addAllTypeAlias(typeAliases) + } + if (sealedSubclassNames.size != sealedSubclassFqNameCount) { + clearSealedSubclassFqName().addAllSealedSubclassFqName(sealedSubclassNames) + } + }.build() +} + +/** + * Removes elements from a [kotlin.Metadata] annotation that contains + * a [ProtoBuf.Package] object in its [d1][kotlin.Metadata.d1] field. + */ +internal class PackageMetadataTransformer( + logger: Logger, + deletedFields: Collection, + deletedFunctions: Collection, + handleExtraMethod: (MethodElement) -> Unit, + d1: List, + d2: List +) : MetadataTransformer( + logger, + deletedFields, + deletedFunctions, + emptyList(), + emptyList(), + emptyList(), + handleExtraMethod, + d1, + d2, + ProtoBuf.Package::parseFrom +) { + override val typeTable = TypeTable(message.typeTable) + override val properties = mutableList(message.propertyList) + override val functions = mutableList(message.functionList) + override val typeAliases = mutableList(message.typeAliasList) + + override fun rebuild(): ProtoBuf.Package = message.toBuilder().apply { + if (functions.size != functionCount) { + clearFunction().addAllFunction(functions) + } + if (properties.size != propertyCount) { + clearProperty().addAllProperty(properties) + } + if (typeAliases.size != typeAliasCount) { + clearTypeAlias().addAllTypeAlias(typeAliases) + } + }.build() +} diff --git a/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/Repeatable.kt b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/Repeatable.kt new file mode 100644 index 0000000000..4123fb3de4 --- /dev/null +++ b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/Repeatable.kt @@ -0,0 +1,8 @@ +package net.corda.gradle.jarfilter + +import org.objectweb.asm.ClassVisitor + +interface Repeatable { + fun recreate(visitor: ClassVisitor): T + val hasUnwantedElements: Boolean +} diff --git a/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/Utils.kt b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/Utils.kt new file mode 100644 index 0000000000..cbbc026c99 --- /dev/null +++ b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/Utils.kt @@ -0,0 +1,89 @@ +@file:JvmName("Utils") +package net.corda.gradle.jarfilter + +import org.gradle.api.GradleException +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.ClassWriter +import java.nio.file.attribute.FileTime +import java.util.* +import java.util.Calendar.FEBRUARY +import java.util.zip.ZipEntry +import java.util.zip.ZipEntry.DEFLATED +import java.util.zip.ZipEntry.STORED +import kotlin.math.max +import kotlin.text.RegexOption.* + +internal val JAR_PATTERN = "(\\.jar)$".toRegex(IGNORE_CASE) + +// Use the same constant file timestamp as Gradle. +private val CONSTANT_TIME: FileTime = FileTime.fromMillis( + GregorianCalendar(1980, FEBRUARY, 1).apply { timeZone = TimeZone.getTimeZone("UTC") }.timeInMillis +) + +internal fun rethrowAsUncheckedException(e: Exception): Nothing + = throw (e as? RuntimeException) ?: GradleException(e.message ?: "", e) + +/** + * Recreates a [ZipEntry] object. The entry's byte contents + * will be compressed automatically, and its CRC, size and + * compressed size fields populated. + */ +internal fun ZipEntry.asCompressed(): ZipEntry { + return ZipEntry(name).also { entry -> + entry.lastModifiedTime = lastModifiedTime + lastAccessTime?.also { at -> entry.lastAccessTime = at } + creationTime?.also { ct -> entry.creationTime = ct } + entry.comment = comment + entry.method = DEFLATED + entry.extra = extra + } +} + +internal fun ZipEntry.copy(): ZipEntry { + return if (method == STORED) ZipEntry(this) else asCompressed() +} + +internal fun ZipEntry.withFileTimestamps(preserveTimestamps: Boolean): ZipEntry { + if (!preserveTimestamps) { + lastModifiedTime = CONSTANT_TIME + lastAccessTime?.apply { lastAccessTime = CONSTANT_TIME } + creationTime?.apply { creationTime = CONSTANT_TIME } + } + return this +} + +internal fun mutableList(c: Collection): MutableList = ArrayList(c) + +/** + * Converts Java class names to Java descriptors. + */ +internal fun toDescriptors(classNames: Iterable): Set { + return classNames.map(String::descriptor).toSet() +} + +internal val String.toPathFormat: String get() = replace('.', '/') +internal val String.descriptor: String get() = "L$toPathFormat;" + + +/** + * Performs the given number of passes of the repeatable visitor over the byte-code. + * Used by [MetaFixerVisitor], but also by some of the test visitors. + */ +internal fun ByteArray.execute(visitor: (ClassVisitor) -> T, flags: Int = 0, passes: Int = 2): ByteArray + where T : ClassVisitor, + T : Repeatable { + var reader = ClassReader(this) + var writer = ClassWriter(flags) + val transformer = visitor(writer) + var count = max(passes, 1) + + reader.accept(transformer, 0) + while (transformer.hasUnwantedElements && --count > 0) { + reader = ClassReader(writer.toByteArray()) + writer = ClassWriter(flags) + reader.accept(transformer.recreate(writer), 0) + } + + return writer.toByteArray() +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/AbstractFunctionTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/AbstractFunctionTest.kt new file mode 100644 index 0000000000..fb4bbc63a0 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/AbstractFunctionTest.kt @@ -0,0 +1,69 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.matcher.* +import org.hamcrest.core.IsCollectionContaining.hasItem +import org.hamcrest.core.IsNot.not +import org.junit.Assert.* +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import java.lang.reflect.Modifier.* +import kotlin.reflect.full.declaredFunctions +import kotlin.test.assertFailsWith + +class AbstractFunctionTest { + companion object { + private const val FUNCTION_CLASS = "net.corda.gradle.AbstractFunctions" + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "abstract-function") + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + } + + @Test + fun deleteAbstractFunction() { + val longFunction = isFunction("toDelete", Long::class, Long::class) + + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + getMethod("toDelete", Long::class.java).also { method -> + assertEquals(ABSTRACT, method.modifiers and ABSTRACT) + } + assertThat("toDelete(J) not found", kotlin.declaredFunctions, hasItem(longFunction)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + assertFailsWith { getMethod("toDelete", Long::class.java) } + assertThat("toDelete(J) still exists", kotlin.declaredFunctions, not(hasItem(longFunction))) + } + } + } + + @Test + fun cannotStubAbstractFunction() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + getMethod("toStubOut", Long::class.java).also { method -> + assertEquals(ABSTRACT, method.modifiers and ABSTRACT) + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + getMethod("toStubOut", Long::class.java).also { method -> + assertEquals(ABSTRACT, method.modifiers and ABSTRACT) + } + } + } + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteAndStubTests.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteAndStubTests.kt new file mode 100644 index 0000000000..40e61b2706 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteAndStubTests.kt @@ -0,0 +1,188 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.matcher.* +import net.corda.gradle.unwanted.* +import org.assertj.core.api.Assertions.* +import org.hamcrest.core.IsCollectionContaining.* +import org.hamcrest.core.IsNot.* +import org.junit.Assert.* +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import kotlin.reflect.full.declaredMemberFunctions +import kotlin.reflect.full.declaredMemberProperties +import kotlin.test.assertFailsWith + +class DeleteAndStubTests { + companion object { + private const val VAR_PROPERTY_CLASS = "net.corda.gradle.HasVarPropertyForDeleteAndStub" + private const val VAL_PROPERTY_CLASS = "net.corda.gradle.HasValPropertyForDeleteAndStub" + private const val DELETED_FUN_CLASS = "net.corda.gradle.DeletedFunctionInsideStubbed" + private const val DELETED_VAR_CLASS = "net.corda.gradle.DeletedVarInsideStubbed" + private const val DELETED_VAL_CLASS = "net.corda.gradle.DeletedValInsideStubbed" + private const val DELETED_PKG_CLASS = "net.corda.gradle.DeletePackageWithStubbed" + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "delete-and-stub") + private val stringVal = isProperty("stringVal", String::class) + private val longVar = isProperty("longVar", Long::class) + private val getStringVal = isMethod("getStringVal", String::class.java) + private val getLongVar = isMethod("getLongVar", Long::class.java) + private val setLongVar = isMethod("setLongVar", Void.TYPE, Long::class.java) + private val stringData = isFunction("stringData", String::class) + private val unwantedFun = isFunction("unwantedFun", String::class, String::class) + private val unwantedVar = isProperty("unwantedVar", String::class) + private val unwantedVal = isProperty("unwantedVal", String::class) + private val stringDataJava = isMethod("stringData", String::class.java) + private val getUnwantedVal = isMethod("getUnwantedVal", String::class.java) + private val getUnwantedVar = isMethod("getUnwantedVar", String::class.java) + private val setUnwantedVar = isMethod("setUnwantedVar", Void.TYPE, String::class.java) + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + } + + @Test + fun deleteValProperty() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(VAL_PROPERTY_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj -> + assertEquals(MESSAGE, obj.stringVal) + } + assertThat("stringVal not found", kotlin.declaredMemberProperties, hasItem(stringVal)) + assertThat("getStringVal() not found", kotlin.javaDeclaredMethods, hasItem(getStringVal)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(VAL_PROPERTY_CLASS).apply { + assertNotNull(getDeclaredConstructor(String::class.java).newInstance(MESSAGE)) + assertThat("stringVal still exists", kotlin.declaredMemberProperties, not(hasItem(stringVal))) + assertThat("getStringVal() still exists", kotlin.javaDeclaredMethods, not(hasItem(getStringVal))) + } + } + } + + @Test + fun deleteVarProperty() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(VAR_PROPERTY_CLASS).apply { + getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER).also { obj -> + assertEquals(BIG_NUMBER, obj.longVar) + } + assertThat("longVar not found", kotlin.declaredMemberProperties, hasItem(longVar)) + assertThat("getLongVar() not found", kotlin.javaDeclaredMethods, hasItem(getLongVar)) + assertThat("setLongVar() not found", kotlin.javaDeclaredMethods, hasItem(setLongVar)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(VAR_PROPERTY_CLASS).apply { + assertNotNull(getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER)) + assertThat("longVar still exists", kotlin.declaredMemberProperties, not(hasItem(longVar))) + assertThat("getLongVar() still exists", kotlin.javaDeclaredMethods, not(hasItem(getLongVar))) + assertThat("setLongVar() still exists", kotlin.javaDeclaredMethods, not(hasItem(setLongVar))) + } + } + } + + @Test + fun deletedFunctionInsideStubbed() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(DELETED_FUN_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(DEFAULT_MESSAGE).also { obj -> + assertEquals(DEFAULT_MESSAGE, obj.stringData()) + assertEquals(MESSAGE, (obj as HasUnwantedFun).unwantedFun(MESSAGE)) + } + assertThat("unwantedFun not found", kotlin.declaredMemberFunctions, hasItem(unwantedFun)) + assertThat("stringData() not found", kotlin.declaredMemberFunctions, hasItem(stringData)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(DELETED_FUN_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(DEFAULT_MESSAGE).also { obj -> + assertFailsWith { obj.stringData() }.also { ex -> + assertThat(ex).hasMessage("Method has been deleted") + } + assertFailsWith { (obj as HasUnwantedFun).unwantedFun(MESSAGE) } + } + assertThat("unwantedFun still exists", kotlin.declaredMemberFunctions, not(hasItem(unwantedFun))) + assertThat("stringData() not found", kotlin.declaredMemberFunctions, hasItem(stringData)) + } + } + } + + @Test + fun deletedVarInsideStubbed() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(DELETED_VAR_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(DEFAULT_MESSAGE).also { obj -> + assertEquals(DEFAULT_MESSAGE, obj.stringData()) + (obj as HasUnwantedVar).also { + assertEquals(DEFAULT_MESSAGE, it.unwantedVar) + it.unwantedVar = MESSAGE + assertEquals(MESSAGE, it.unwantedVar) + } + } + assertThat("unwantedVar not found", kotlin.declaredMemberProperties, hasItem(unwantedVar)) + assertThat("stringData() not found", kotlin.declaredMemberFunctions, hasItem(stringData)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(DELETED_VAR_CLASS).apply { + assertNotNull(getDeclaredConstructor(String::class.java).newInstance(MESSAGE)) + assertThat("unwantedVar still exists", kotlin.declaredMemberProperties, not(hasItem(unwantedVar))) + assertThat("getUnwantedVar() still exists", kotlin.javaDeclaredMethods, not(hasItem(getUnwantedVar))) + assertThat("setUnwantedVar() still exists", kotlin.javaDeclaredMethods, not(hasItem(setUnwantedVar))) + assertThat("stringData() not found", kotlin.declaredMemberFunctions, hasItem(stringData)) + assertThat("stringData() not found", kotlin.javaDeclaredMethods, hasItem(stringDataJava)) + } + } + } + + @Test + fun deletedValInsideStubbed() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(DELETED_VAL_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj -> + assertEquals(MESSAGE, obj.stringData()) + assertEquals(MESSAGE, (obj as HasUnwantedVal).unwantedVal) + } + assertThat("unwantedVal not found", kotlin.declaredMemberProperties, hasItem(unwantedVal)) + assertThat("stringData() not found", kotlin.declaredMemberFunctions, hasItem(stringData)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(DELETED_VAL_CLASS).apply { + assertNotNull(getDeclaredConstructor(String::class.java).newInstance(MESSAGE)) + assertThat("unwantedVal still exists", kotlin.declaredMemberProperties, not(hasItem(unwantedVal))) + assertThat("getUnwantedVal() still exists", kotlin.javaDeclaredMethods, not(hasItem(getUnwantedVal))) + assertThat("stringData() not found", kotlin.declaredMemberFunctions, hasItem(stringData)) + assertThat("stringData() not found", kotlin.javaDeclaredMethods, hasItem(stringDataJava)) + } + } + } + + @Test + fun deletePackageWithStubbed() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(DELETED_PKG_CLASS).apply { + getDeclaredMethod("stubbed", String::class.java).also { method -> + assertEquals("[$MESSAGE]", method.invoke(null, MESSAGE)) + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + assertFailsWith { cl.load(DELETED_PKG_CLASS) } + } + } +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteConstructorTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteConstructorTest.kt new file mode 100644 index 0000000000..08599d0865 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteConstructorTest.kt @@ -0,0 +1,165 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.matcher.* +import net.corda.gradle.unwanted.HasAll +import net.corda.gradle.unwanted.HasInt +import net.corda.gradle.unwanted.HasLong +import net.corda.gradle.unwanted.HasString +import org.hamcrest.core.IsCollectionContaining.hasItem +import org.hamcrest.core.IsNot.not +import org.junit.Assert.* +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import kotlin.jvm.kotlin +import kotlin.reflect.full.primaryConstructor +import kotlin.test.assertFailsWith + +class DeleteConstructorTest { + companion object { + private const val STRING_PRIMARY_CONSTRUCTOR_CLASS = "net.corda.gradle.PrimaryStringConstructorToDelete" + private const val LONG_PRIMARY_CONSTRUCTOR_CLASS = "net.corda.gradle.PrimaryLongConstructorToDelete" + private const val INT_PRIMARY_CONSTRUCTOR_CLASS = "net.corda.gradle.PrimaryIntConstructorToDelete" + private const val SECONDARY_CONSTRUCTOR_CLASS = "net.corda.gradle.HasConstructorToDelete" + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "delete-constructor") + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + } + + @Test + fun deleteConstructorWithLongParameter() { + val longConstructor = isConstructor(SECONDARY_CONSTRUCTOR_CLASS, hasParam(Long::class)) + + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(SECONDARY_CONSTRUCTOR_CLASS).apply { + getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER).also { + assertEquals(BIG_NUMBER, it.longData()) + } + assertThat("(J) not found", kotlin.constructors, hasItem(longConstructor)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(SECONDARY_CONSTRUCTOR_CLASS).apply { + assertFailsWith { getDeclaredConstructor(Long::class.java) } + assertThat("(J) still exists", kotlin.constructors, not(hasItem(longConstructor))) + assertNotNull("primary constructor missing", kotlin.primaryConstructor) + } + } + } + + @Test + fun deleteConstructorWithStringParameter() { + val stringConstructor = isConstructor(SECONDARY_CONSTRUCTOR_CLASS, hasParam(String::class)) + + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(SECONDARY_CONSTRUCTOR_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { + assertEquals(MESSAGE, it.stringData()) + } + assertThat("(String) not found", kotlin.constructors, hasItem(stringConstructor)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(SECONDARY_CONSTRUCTOR_CLASS).apply { + assertFailsWith { getDeclaredConstructor(String::class.java) } + assertThat("(String) still exists", kotlin.constructors, not(hasItem(stringConstructor))) + assertNotNull("primary constructor missing", kotlin.primaryConstructor) + } + } + } + + @Test + fun showUnannotatedConstructorIsUnaffected() { + val intConstructor = isConstructor(SECONDARY_CONSTRUCTOR_CLASS, hasParam(Int::class)) + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(SECONDARY_CONSTRUCTOR_CLASS).apply { + getDeclaredConstructor(Int::class.java).newInstance(NUMBER).also { + assertEquals(NUMBER, it.intData()) + assertEquals(NUMBER.toLong(), it.longData()) + assertEquals("", it.stringData()) + } + assertThat("(Int) not found", kotlin.constructors, hasItem(intConstructor)) + assertNotNull("primary constructor missing", kotlin.primaryConstructor) + } + } + } + + @Test + fun deletePrimaryConstructorWithStringParameter() { + val stringConstructor = isConstructor(STRING_PRIMARY_CONSTRUCTOR_CLASS, hasParam(String::class)) + + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(STRING_PRIMARY_CONSTRUCTOR_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { + assertEquals(MESSAGE, it.stringData()) + } + assertThat("(String) not found", kotlin.constructors, hasItem(stringConstructor)) + assertThat("primary constructor missing", kotlin.primaryConstructor!!, stringConstructor) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(STRING_PRIMARY_CONSTRUCTOR_CLASS).apply { + assertFailsWith { getDeclaredConstructor(String::class.java) } + assertThat("(String) still exists", kotlin.constructors, not(hasItem(stringConstructor))) + assertNull("primary constructor still exists", kotlin.primaryConstructor) + } + } + } + + @Test + fun deletePrimaryConstructorWithLongParameter() { + val longConstructor = isConstructor(LONG_PRIMARY_CONSTRUCTOR_CLASS, hasParam(Long::class)) + + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(LONG_PRIMARY_CONSTRUCTOR_CLASS).apply { + getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER).also { + assertEquals(BIG_NUMBER, it.longData()) + } + assertThat("(J) not found", kotlin.constructors, hasItem(longConstructor)) + assertThat("primary constructor missing", kotlin.primaryConstructor!!, longConstructor) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(LONG_PRIMARY_CONSTRUCTOR_CLASS).apply { + assertFailsWith { getDeclaredConstructor(Long::class.java) } + assertThat("(J) still exists", kotlin.constructors, not(hasItem(longConstructor))) + assertNull("primary constructor still exists", kotlin.primaryConstructor) + } + } + } + + @Test + fun deletePrimaryConstructorWithIntParameter() { + val intConstructor = isConstructor(INT_PRIMARY_CONSTRUCTOR_CLASS, hasParam(Int::class)) + + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(INT_PRIMARY_CONSTRUCTOR_CLASS).apply { + getDeclaredConstructor(Int::class.java).newInstance(NUMBER).also { + assertEquals(NUMBER, it.intData()) + } + assertThat("(I) not found", kotlin.constructors, hasItem(intConstructor)) + assertThat("primary constructor missing", kotlin.primaryConstructor!!, intConstructor) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(INT_PRIMARY_CONSTRUCTOR_CLASS).apply { + assertFailsWith { getDeclaredConstructor(Int::class.java) } + assertThat("(I) still exists", kotlin.constructors, not(hasItem(intConstructor))) + assertNull("primary constructor still exists", kotlin.primaryConstructor) + } + } + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteExtensionValPropertyTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteExtensionValPropertyTest.kt new file mode 100644 index 0000000000..4723caef19 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteExtensionValPropertyTest.kt @@ -0,0 +1,52 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.matcher.isMethod +import net.corda.gradle.jarfilter.matcher.isProperty +import net.corda.gradle.jarfilter.matcher.javaDeclaredMethods +import net.corda.gradle.unwanted.HasUnwantedVal +import org.hamcrest.core.IsCollectionContaining.* +import org.hamcrest.core.IsNot.* +import org.junit.Assert.* +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import kotlin.reflect.full.declaredMemberExtensionProperties +import kotlin.reflect.full.declaredMemberProperties + +class DeleteExtensionValPropertyTest { + companion object { + private const val PROPERTY_CLASS = "net.corda.gradle.HasValExtension" + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "delete-extension-val") + private val unwantedVal = isProperty("unwantedVal", String::class) + private val getUnwantedVal = isMethod("getUnwantedVal", String::class.java, List::class.java) + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + } + + @Test + fun deleteExtensionProperty() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + assertThat("unwantedVal not found", kotlin.declaredMemberProperties, hasItem(unwantedVal)) + assertThat("getUnwantedVal not found", kotlin.javaDeclaredMethods, hasItem(getUnwantedVal)) + assertThat("List.unwantedVal not found", kotlin.declaredMemberExtensionProperties, hasItem(unwantedVal)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + assertThat("unwantedVal not found", kotlin.declaredMemberProperties, hasItem(unwantedVal)) + assertThat("getUnwantedVal still exists", kotlin.javaDeclaredMethods, not(hasItem(getUnwantedVal))) + assertThat("List.unwantedVal still exists", kotlin.declaredMemberExtensionProperties, not(hasItem(unwantedVal))) + } + } + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteFieldTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteFieldTest.kt new file mode 100644 index 0000000000..4be9530f20 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteFieldTest.kt @@ -0,0 +1,99 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.matcher.* +import org.hamcrest.core.IsCollectionContaining.hasItem +import org.hamcrest.core.IsNot.not +import org.junit.Assert.* +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import kotlin.reflect.full.declaredMemberProperties +import kotlin.test.assertFailsWith + +class DeleteFieldTest { + companion object { + private const val STRING_FIELD_CLASS = "net.corda.gradle.HasStringFieldToDelete" + private const val INTEGER_FIELD_CLASS = "net.corda.gradle.HasIntFieldToDelete" + private const val LONG_FIELD_CLASS = "net.corda.gradle.HasLongFieldToDelete" + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "delete-field") + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + } + + @Test + fun deleteStringField() { + val stringField = isProperty("stringField", String::class) + + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(STRING_FIELD_CLASS).apply { + val obj: Any = getDeclaredConstructor(String::class.java).newInstance(MESSAGE) + getDeclaredField("stringField").also { field -> + assertEquals(MESSAGE, field.get(obj)) + } + assertThat("stringField not found", kotlin.declaredMemberProperties, hasItem(stringField)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(STRING_FIELD_CLASS).apply { + assertNotNull(getDeclaredConstructor(String::class.java).newInstance(MESSAGE)) + assertFailsWith { getDeclaredField("stringField") } + assertThat("stringField still exists", kotlin.declaredMemberProperties, not(hasItem(stringField))) + } + } + } + + @Test + fun deleteLongField() { + val longField = isProperty("longField", Long::class) + + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(LONG_FIELD_CLASS).apply { + val obj: Any = getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER) + getDeclaredField("longField").also { field -> + assertEquals(BIG_NUMBER, field.get(obj)) + } + assertThat("longField not found", kotlin.declaredMemberProperties, hasItem(longField)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(LONG_FIELD_CLASS).apply { + assertNotNull(getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER)) + assertFailsWith { getDeclaredField("longField") } + assertThat("longField still exists", kotlin.declaredMemberProperties, not(hasItem(longField))) + } + } + } + + @Test + fun deleteIntegerField() { + val intField = isProperty("intField", Int::class) + + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(INTEGER_FIELD_CLASS).apply { + val obj: Any = getDeclaredConstructor(Int::class.java).newInstance(NUMBER) + getDeclaredField("intField").also { field -> + assertEquals(NUMBER, field.get(obj)) + } + assertThat("intField not found", kotlin.declaredMemberProperties, hasItem(intField)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(INTEGER_FIELD_CLASS).apply { + assertNotNull(getDeclaredConstructor(Int::class.java).newInstance(NUMBER)) + assertFailsWith { getDeclaredField("intField") } + assertThat("intField still exists", kotlin.declaredMemberProperties, not(hasItem(intField))) + } + } + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteFunctionTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteFunctionTest.kt new file mode 100644 index 0000000000..8e035fead8 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteFunctionTest.kt @@ -0,0 +1,81 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.matcher.* +import net.corda.gradle.unwanted.HasString +import net.corda.gradle.unwanted.HasUnwantedFun +import org.hamcrest.core.IsCollectionContaining.hasItem +import org.hamcrest.core.IsNot.not +import org.junit.Assert.* +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import kotlin.jvm.kotlin +import kotlin.reflect.full.declaredFunctions +import kotlin.test.assertFailsWith + +class DeleteFunctionTest { + companion object { + private const val FUNCTION_CLASS = "net.corda.gradle.HasFunctionToDelete" + private const val INDIRECT_CLASS = "net.corda.gradle.HasIndirectFunctionToDelete" + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "delete-function") + private val unwantedFun = isFunction("unwantedFun", String::class, String::class) + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + } + + @Test + fun deleteFunction() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + newInstance().also { + assertEquals(MESSAGE, it.unwantedFun(MESSAGE)) + } + assertThat("unwantedFun(String) not found", kotlin.declaredFunctions, hasItem(unwantedFun)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + newInstance().also { + assertFailsWith { it.unwantedFun(MESSAGE) } + } + assertThat("unwantedFun(String) still exists", kotlin.declaredFunctions, not(hasItem(unwantedFun))) + } + } + } + + @Test + fun deleteIndirectFunction() { + val stringData = isFunction("stringData", String::class) + + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(INDIRECT_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { + assertEquals(MESSAGE, it.unwantedFun(MESSAGE)) + assertEquals(MESSAGE, (it as HasString).stringData()) + } + assertThat("unwantedFun(String) not found", kotlin.declaredFunctions, hasItem(unwantedFun)) + assertThat("stringData() not found", kotlin.declaredFunctions, hasItem(stringData)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(INDIRECT_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { + assertFailsWith { it.unwantedFun(MESSAGE) } + assertFailsWith { (it as HasString).stringData() } + } + assertThat("unwantedFun(String) still exists", kotlin.declaredFunctions, not(hasItem(unwantedFun))) + assertThat("stringData still exists", kotlin.declaredFunctions, not(hasItem(stringData))) + } + } + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteLazyTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteLazyTest.kt new file mode 100644 index 0000000000..ac217fea87 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteLazyTest.kt @@ -0,0 +1,71 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.matcher.* +import net.corda.gradle.unwanted.HasUnwantedVal +import org.assertj.core.api.Assertions.* +import org.hamcrest.core.IsCollectionContaining.* +import org.hamcrest.core.IsNot.* +import org.junit.Assert.* +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import kotlin.reflect.full.declaredMemberProperties +import kotlin.test.assertFailsWith + +class DeleteLazyTest { + companion object { + private const val LAZY_VAL_CLASS = "net.corda.gradle.HasLazyVal" + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "delete-lazy") + private val unwantedVal = isProperty("unwantedVal", String::class) + private val getUnwantedVal = isMethod("getUnwantedVal", String::class.java) + private lateinit var sourceClasses: List + private lateinit var filteredClasses: List + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + + @BeforeClass + @JvmStatic + fun setup() { + sourceClasses = testProject.sourceJar.getClassNames(LAZY_VAL_CLASS) + filteredClasses = testProject.filteredJar.getClassNames(LAZY_VAL_CLASS) + } + } + + @Test + fun deletedClasses() { + assertThat(sourceClasses).contains(LAZY_VAL_CLASS) + assertThat(filteredClasses).containsExactly(LAZY_VAL_CLASS) + } + + @Test + fun deleteLazyVal() { + assertThat(sourceClasses).anyMatch { it.contains("\$unwantedVal\$") } + + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(LAZY_VAL_CLASS).apply { + getConstructor(String::class.java).newInstance(MESSAGE).also { obj -> + assertEquals(MESSAGE, obj.unwantedVal) + } + assertThat("getUnwantedVal not found", kotlin.javaDeclaredMethods, hasItem(getUnwantedVal)) + assertThat("unwantedVal not found", kotlin.declaredMemberProperties, hasItem(unwantedVal)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(LAZY_VAL_CLASS).apply { + assertFailsWith { getConstructor(String::class.java) } + assertThat("getUnwantedVal still exists", kotlin.javaDeclaredMethods, not(hasItem(getUnwantedVal))) + assertThat("unwantedVal still exists", kotlin.declaredMemberProperties, not(hasItem(unwantedVal))) + } + } + } +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteMultiFileTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteMultiFileTest.kt new file mode 100644 index 0000000000..4c7333c41a --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteMultiFileTest.kt @@ -0,0 +1,90 @@ +package net.corda.gradle.jarfilter + +import org.assertj.core.api.Assertions.* +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import kotlin.test.assertFailsWith + +class DeleteMultiFileTest { + companion object { + private const val MULTIFILE_CLASS = "net.corda.gradle.HasMultiData" + private const val STRING_METHOD = "stringToDelete" + private const val LONG_METHOD = "longToDelete" + private const val INT_METHOD = "intToDelete" + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "delete-multifile") + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + } + + @Test + fun deleteStringFunction() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(MULTIFILE_CLASS).apply { + getMethod(STRING_METHOD, String::class.java).also { method -> + method.invoke(null, MESSAGE).also { result -> + assertThat(result) + .isInstanceOf(String::class.java) + .isEqualTo(MESSAGE) + } + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(MULTIFILE_CLASS).apply { + assertFailsWith { getMethod(STRING_METHOD, String::class.java) } + } + } + } + + @Test + fun deleteLongFunction() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(MULTIFILE_CLASS).apply { + getMethod(LONG_METHOD, Long::class.java).also { method -> + method.invoke(null, BIG_NUMBER).also { result -> + assertThat(result) + .isInstanceOf(Long::class.javaObjectType) + .isEqualTo(BIG_NUMBER) + } + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(MULTIFILE_CLASS).apply { + assertFailsWith { getMethod(LONG_METHOD, Long::class.java) } + } + } + } + + @Test + fun deleteIntFunction() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(MULTIFILE_CLASS).apply { + getMethod(INT_METHOD, Int::class.java).also { method -> + method.invoke(null, NUMBER).also { result -> + assertThat(result) + .isInstanceOf(Int::class.javaObjectType) + .isEqualTo(NUMBER) + } + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(MULTIFILE_CLASS).apply { + assertFailsWith { getMethod(INT_METHOD, Int::class.java) } + } + } + } +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteNestedClassTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteNestedClassTest.kt new file mode 100644 index 0000000000..934a2797bc --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteNestedClassTest.kt @@ -0,0 +1,90 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.asm.classMetadata +import net.corda.gradle.jarfilter.matcher.isClass +import org.assertj.core.api.Assertions.* +import org.hamcrest.core.IsCollectionContaining.hasItem +import org.hamcrest.core.IsNot.not +import org.junit.Assert.* +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import kotlin.test.assertFailsWith + +class DeleteNestedClassTest { + companion object { + private const val HOST_CLASS = "net.corda.gradle.HasNestedClasses" + private const val KEPT_CLASS = "$HOST_CLASS\$OneToKeep" + private const val DELETED_CLASS = "$HOST_CLASS\$OneToThrowAway" + + private const val SEALED_CLASS = "net.corda.gradle.SealedClass" + private const val WANTED_SUBCLASS = "$SEALED_CLASS\$Wanted" + private const val UNWANTED_SUBCLASS = "$SEALED_CLASS\$Unwanted" + + private val keptClass = isClass(KEPT_CLASS) + private val deletedClass = isClass(DELETED_CLASS) + private val wantedSubclass = isClass(WANTED_SUBCLASS) + private val unwantedSubclass = isClass(UNWANTED_SUBCLASS) + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "delete-nested-class") + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + } + + @Test + fun deleteNestedClass() { + classLoaderFor(testProject.sourceJar).use { cl -> + val deleted = cl.load(DELETED_CLASS) + val kept = cl.load(KEPT_CLASS) + cl.load(HOST_CLASS).apply { + assertThat(declaredClasses).containsExactlyInAnyOrder(deleted, kept) + assertThat("OneToThrowAway class is missing", kotlin.nestedClasses, hasItem(deletedClass)) + assertThat("OneToKeep class is missing", kotlin.nestedClasses, hasItem(keptClass)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + assertFailsWith { cl.load(DELETED_CLASS) } + val kept = cl.load(KEPT_CLASS) + cl.load(HOST_CLASS).apply { + assertThat(declaredClasses).containsExactly(kept) + assertThat("OneToThrowAway class still exists", kotlin.nestedClasses, not(hasItem(deletedClass))) + assertThat("OneToKeep class is missing", kotlin.nestedClasses, hasItem(keptClass)) + } + } + } + + @Test + fun deleteFromSealedClass() { + classLoaderFor(testProject.sourceJar).use { cl -> + val unwanted = cl.load(UNWANTED_SUBCLASS) + val wanted = cl.load(WANTED_SUBCLASS) + cl.load(SEALED_CLASS).apply { + assertTrue(kotlin.isSealed) + assertThat(declaredClasses).containsExactlyInAnyOrder(wanted, unwanted) + assertThat("Wanted class is missing", kotlin.nestedClasses, hasItem(wantedSubclass)) + assertThat("Unwanted class is missing", kotlin.nestedClasses, hasItem(unwantedSubclass)) + assertThat(classMetadata.sealedSubclasses).containsExactlyInAnyOrder(WANTED_SUBCLASS, UNWANTED_SUBCLASS) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + assertFailsWith { cl.load(UNWANTED_SUBCLASS) } + val wanted = cl.load(WANTED_SUBCLASS) + cl.load(SEALED_CLASS).apply { + assertTrue(kotlin.isSealed) + assertThat(declaredClasses).containsExactly(wanted) + assertThat("Unwanted class still exists", kotlin.nestedClasses, not(hasItem(unwantedSubclass))) + assertThat("Wanted class is missing", kotlin.nestedClasses, hasItem(wantedSubclass)) + assertThat(classMetadata.sealedSubclasses).containsExactly(WANTED_SUBCLASS) + } + } + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteObjectTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteObjectTest.kt new file mode 100644 index 0000000000..5e6db4a4b1 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteObjectTest.kt @@ -0,0 +1,89 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.unwanted.HasUnwantedFun +import org.assertj.core.api.Assertions.* +import org.junit.Assert.* +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import kotlin.test.assertFailsWith + +class DeleteObjectTest { + companion object { + private const val OBJECT_CLASS = "net.corda.gradle.HasObjects" + private const val UNWANTED_OBJ_METHOD = "getUnwantedObj" + private const val UNWANTED_OBJ_FIELD = "unwantedObj" + private const val UNWANTED_FUN_METHOD = "unwantedFun" + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "delete-object") + private lateinit var sourceClasses: List + private lateinit var filteredClasses: List + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + + @BeforeClass + @JvmStatic + fun setup() { + sourceClasses = testProject.sourceJar.getClassNames(OBJECT_CLASS) + filteredClasses = testProject.filteredJar.getClassNames(OBJECT_CLASS) + } + } + + @Test + fun deletedClasses() { + assertThat(sourceClasses).contains(OBJECT_CLASS) + assertThat(filteredClasses).containsExactly(OBJECT_CLASS) + } + + @Test + fun deleteObject() { + assertThat(sourceClasses).anyMatch { it.contains("\$unwantedObj\$") } + + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(OBJECT_CLASS).apply { + getDeclaredMethod(UNWANTED_OBJ_METHOD).also { method -> + (method.invoke(null) as HasUnwantedFun).also { obj -> + assertEquals(MESSAGE, obj.unwantedFun(MESSAGE)) + } + } + getDeclaredField(UNWANTED_OBJ_FIELD).also { field -> + assertFalse(field.isAccessible) + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(OBJECT_CLASS).apply { + assertFailsWith { getDeclaredMethod(UNWANTED_OBJ_METHOD) } + assertFailsWith { getDeclaredField(UNWANTED_OBJ_FIELD) } + } + } + } + + @Test + fun deleteFunctionWithObject() { + assertThat(sourceClasses).anyMatch { it.contains("\$unwantedFun\$") } + + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(OBJECT_CLASS).apply { + getDeclaredMethod(UNWANTED_FUN_METHOD).also { method -> + assertEquals("", method.invoke(null)) + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(OBJECT_CLASS).apply { + assertFailsWith { getDeclaredMethod(UNWANTED_FUN_METHOD) } + } + } + } +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteSealedSubclassTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteSealedSubclassTest.kt new file mode 100644 index 0000000000..eeb90e4c7f --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteSealedSubclassTest.kt @@ -0,0 +1,56 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.asm.classMetadata +import org.assertj.core.api.Assertions.assertThat +import org.junit.Assert.* +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import kotlin.test.assertFailsWith + +/** + * Sealed classes can have non-nested subclasses, so long as those subclasses + * are declared in the same file as the sealed class. Check that the metadata + * is still updated correctly in this case. + */ +class DeleteSealedSubclassTest { + companion object { + private const val SEALED_CLASS = "net.corda.gradle.SealedBaseClass" + private const val WANTED_SUBCLASS = "net.corda.gradle.WantedSubclass" + private const val UNWANTED_SUBCLASS = "net.corda.gradle.UnwantedSubclass" + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "delete-sealed-subclass") + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + } + + @Test + fun deleteUnwantedSubclass() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(WANTED_SUBCLASS) + cl.load(UNWANTED_SUBCLASS) + cl.load(SEALED_CLASS).apply { + assertTrue(kotlin.isSealed) + assertThat(classMetadata.sealedSubclasses) + .containsExactlyInAnyOrder(WANTED_SUBCLASS, UNWANTED_SUBCLASS) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(WANTED_SUBCLASS) + assertFailsWith { cl.load(UNWANTED_SUBCLASS) } + cl.load(SEALED_CLASS).apply { + assertTrue(kotlin.isSealed) + assertThat(classMetadata.sealedSubclasses) + .containsExactly(WANTED_SUBCLASS) + } + } + } +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteStaticFieldTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteStaticFieldTest.kt new file mode 100644 index 0000000000..1b3d668035 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteStaticFieldTest.kt @@ -0,0 +1,74 @@ +package net.corda.gradle.jarfilter + +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class DeleteStaticFieldTest { + companion object { + private const val PROPERTY_CLASS = "net.corda.gradle.StaticFieldsToDelete" + private const val DEFAULT_BIG_NUMBER: Long = 123456789L + private const val DEFAULT_NUMBER: Int = 123456 + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "delete-static-field") + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + } + + @Test + fun deleteStringField() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + val getter = getDeclaredField("stringField") + assertEquals(DEFAULT_MESSAGE, getter.get(null)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + assertFailsWith { getDeclaredField("stringField") } + } + } + } + + @Test + fun deleteLongField() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + val getter = getDeclaredField("longField") + assertEquals(DEFAULT_BIG_NUMBER, getter.get(null)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + assertFailsWith { getDeclaredField("longField") } + } + } + } + + @Test + fun deleteIntField() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + val getter = getDeclaredField("intField") + assertEquals(DEFAULT_NUMBER, getter.get(null)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + assertFailsWith { getDeclaredField("intField") } + } + } + } +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteStaticFunctionTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteStaticFunctionTest.kt new file mode 100644 index 0000000000..5ad8102dfb --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteStaticFunctionTest.kt @@ -0,0 +1,87 @@ +package net.corda.gradle.jarfilter + +import org.assertj.core.api.Assertions.* +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import kotlin.test.assertFailsWith + +class DeleteStaticFunctionTest { + companion object { + private const val FUNCTION_CLASS = "net.corda.gradle.StaticFunctionsToDelete" + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "delete-static-function") + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + } + + @Test + fun deleteStringFunction() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + getMethod("unwantedStringToDelete", String::class.java).also { method -> + method.invoke(null, MESSAGE).also { result -> + assertThat(result) + .isInstanceOf(String::class.java) + .isEqualTo(MESSAGE) + } + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + assertFailsWith { getMethod("unwantedStringToDelete", String::class.java) } + } + } + } + + @Test + fun deleteLongFunction() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + getMethod("unwantedLongToDelete", Long::class.java).also { method -> + method.invoke(null, BIG_NUMBER).also { result -> + assertThat(result) + .isInstanceOf(Long::class.javaObjectType) + .isEqualTo(BIG_NUMBER) + } + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + assertFailsWith { getMethod("unwantedLongToDelete", Long::class.java) } + } + } + } + + @Test + fun deleteIntFunction() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + getMethod("unwantedIntToDelete", Int::class.java).also { method -> + method.invoke(null, NUMBER).also { result -> + assertThat(result) + .isInstanceOf(Int::class.javaObjectType) + .isEqualTo(NUMBER) + } + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + assertFailsWith { getMethod("unwantedIntToDelete", Int::class.java) } + } + } + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteStaticValPropertyTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteStaticValPropertyTest.kt new file mode 100644 index 0000000000..e24afa4b99 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteStaticValPropertyTest.kt @@ -0,0 +1,91 @@ +package net.corda.gradle.jarfilter + +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class DeleteStaticValPropertyTest { + companion object { + private const val PROPERTY_CLASS = "net.corda.gradle.StaticValToDelete" + private const val DEFAULT_BIG_NUMBER: Long = 123456789L + private const val DEFAULT_NUMBER: Int = 123456 + private object LocalBlob + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "delete-static-val") + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + } + + @Test + fun deleteStringVal() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + val getter = getDeclaredMethod("getStringVal") + assertEquals(DEFAULT_MESSAGE, getter.invoke(null)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + assertFailsWith { getDeclaredMethod("getStringVal") } + } + } + } + + @Test + fun deleteLongVal() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + val getter = getDeclaredMethod("getLongVal") + assertEquals(DEFAULT_BIG_NUMBER, getter.invoke(null)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + assertFailsWith { getDeclaredMethod("getLongVal") } + } + } + } + + @Test + fun deleteIntVal() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + val getter = getDeclaredMethod("getIntVal") + assertEquals(DEFAULT_NUMBER, getter.invoke(null)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + assertFailsWith { getDeclaredMethod("getIntVal") } + } + } + } + + @Test + fun deleteMemberVal() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + val getter = getDeclaredMethod("getMemberVal", Any::class.java) + assertEquals(LocalBlob, getter.invoke(null, LocalBlob)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + assertFailsWith { getDeclaredMethod("getMemberVal", Any::class.java) } + } + } + } +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteStaticVarPropertyTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteStaticVarPropertyTest.kt new file mode 100644 index 0000000000..9bac98c37b --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteStaticVarPropertyTest.kt @@ -0,0 +1,106 @@ +package net.corda.gradle.jarfilter + +import org.junit.Assert.* +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import kotlin.test.assertFailsWith + +class DeleteStaticVarPropertyTest { + companion object { + private const val PROPERTY_CLASS = "net.corda.gradle.StaticVarToDelete" + private const val DEFAULT_BIG_NUMBER: Long = 123456789L + private const val DEFAULT_NUMBER: Int = 123456 + private object LocalBlob + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "delete-static-var") + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + } + + @Test + fun deleteStringVar() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + val getter = getDeclaredMethod("getStringVar") + val setter = getDeclaredMethod("setStringVar", String::class.java) + assertEquals(DEFAULT_MESSAGE, getter.invoke(null)) + setter.invoke(null, MESSAGE) + assertEquals(MESSAGE, getter.invoke(null)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + assertFailsWith { getDeclaredMethod("getStringVar") } + assertFailsWith { getDeclaredMethod("setStringVar", String::class.java) } + } + } + } + + @Test + fun deleteLongVar() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + val getter = getDeclaredMethod("getLongVar") + val setter = getDeclaredMethod("setLongVar", Long::class.java) + assertEquals(DEFAULT_BIG_NUMBER, getter.invoke(null)) + setter.invoke(null, BIG_NUMBER) + assertEquals(BIG_NUMBER, getter.invoke(null)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + assertFailsWith { getDeclaredMethod("getLongVar") } + assertFailsWith { getDeclaredMethod("setLongVar", Long::class.java) } + } + } + } + + @Test + fun deleteIntVar() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + val getter = getDeclaredMethod("getIntVar") + val setter = getDeclaredMethod("setIntVar", Int::class.java) + assertEquals(DEFAULT_NUMBER, getter.invoke(null)) + setter.invoke(null, NUMBER) + assertEquals(NUMBER, getter.invoke(null)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + assertFailsWith { getDeclaredMethod("getIntVar") } + assertFailsWith { getDeclaredMethod("setIntVar", Int::class.java) } + } + } + } + + @Test + fun deleteMemberVar() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + val getter = getDeclaredMethod("getMemberVar", Any::class.java) + val setter = getDeclaredMethod("setMemberVar", Any::class.java, Any::class.java) + assertEquals(LocalBlob, getter.invoke(null, LocalBlob)) + setter.invoke(null, LocalBlob, LocalBlob) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + assertFailsWith { getDeclaredMethod("getMemberVar", Any::class.java) } + assertFailsWith { getDeclaredMethod("setMemberVar", Any::class.java, Any::class.java) } + } + } + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteTypeAliasFromFileTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteTypeAliasFromFileTest.kt new file mode 100644 index 0000000000..bb2dc6f2a7 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteTypeAliasFromFileTest.kt @@ -0,0 +1,48 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.asm.fileMetadata +import org.assertj.core.api.Assertions.assertThat +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule + +class DeleteTypeAliasFromFileTest { + companion object { + private const val TYPEALIAS_CLASS = "net.corda.gradle.FileWithTypeAlias" + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "delete-file-typealias") + private lateinit var sourceClasses: List + private lateinit var filteredClasses: List + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + + @BeforeClass + @JvmStatic + fun setup() { + sourceClasses = testProject.sourceJar.getClassNames(TYPEALIAS_CLASS) + filteredClasses = testProject.filteredJar.getClassNames(TYPEALIAS_CLASS) + } + } + + @Test + fun deleteTypeAlias() { + classLoaderFor(testProject.sourceJar).use { cl -> + val metadata = cl.load(TYPEALIAS_CLASS).fileMetadata + assertThat(metadata.typeAliasNames) + .containsExactlyInAnyOrder("FileWantedType", "FileUnwantedType") + } + classLoaderFor(testProject.filteredJar).use { cl -> + val metadata = cl.load(TYPEALIAS_CLASS).fileMetadata + assertThat(metadata.typeAliasNames) + .containsExactly("FileWantedType") + } + } +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteValPropertyTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteValPropertyTest.kt new file mode 100644 index 0000000000..75d59b4e54 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteValPropertyTest.kt @@ -0,0 +1,102 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.matcher.* +import net.corda.gradle.unwanted.HasUnwantedVal +import org.hamcrest.core.IsCollectionContaining.* +import org.hamcrest.core.IsNot.* +import org.junit.Assert.* +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import kotlin.reflect.full.declaredMemberProperties +import kotlin.test.assertFailsWith + +class DeleteValPropertyTest { + companion object { + private const val PROPERTY_CLASS = "net.corda.gradle.HasValPropertyForDelete" + private const val GETTER_CLASS = "net.corda.gradle.HasValGetterForDelete" + private const val JVM_FIELD_CLASS = "net.corda.gradle.HasValJvmFieldForDelete" + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "delete-val-property") + private val unwantedVal = isProperty("unwantedVal", String::class) + private val getUnwantedVal = isMethod("getUnwantedVal", String::class.java) + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + } + + @Test + fun deleteProperty() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj -> + assertEquals(MESSAGE, obj.unwantedVal) + } + assertFalse(getDeclaredField("unwantedVal").isAccessible) + assertThat("unwantedVal not found", kotlin.declaredMemberProperties, hasItem(unwantedVal)) + assertThat("getUnwantedVal not found", kotlin.javaDeclaredMethods, hasItem(getUnwantedVal)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj -> + assertFailsWith { obj.unwantedVal } + } + assertFailsWith { getDeclaredField("unwantedVal") } + assertThat("unwantedVal still exists", kotlin.declaredMemberProperties, not(hasItem(unwantedVal))) + assertThat("getUnwantedVal still exists", kotlin.javaDeclaredMethods, not(hasItem(getUnwantedVal))) + } + } + } + + @Test + fun deleteGetter() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(GETTER_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj -> + assertEquals(MESSAGE, obj.unwantedVal) + } + assertFalse(getDeclaredField("unwantedVal").isAccessible) + assertThat("getUnwantedVal not found", kotlin.javaDeclaredMethods, hasItem(getUnwantedVal)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(GETTER_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj -> + assertFailsWith { obj.unwantedVal } + } + assertFalse(getDeclaredField("unwantedVal").isAccessible) + assertThat("getUnwantedVal still exists", kotlin.javaDeclaredMethods, not(hasItem(getUnwantedVal))) + } + } + } + + @Test + fun deleteJvmField() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(JVM_FIELD_CLASS).apply { + val obj = getDeclaredConstructor(String::class.java).newInstance(MESSAGE) + getDeclaredField("unwantedVal").also { field -> + assertEquals(MESSAGE, field.get(obj)) + } + assertThat("unwantedVal not found", kotlin.declaredMemberProperties, hasItem(unwantedVal)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(JVM_FIELD_CLASS).apply { + assertNotNull(getDeclaredConstructor(String::class.java).newInstance(MESSAGE)) + assertFailsWith { getDeclaredField("unwantedVal") } + assertThat("unwantedVal still exists", kotlin.declaredMemberProperties, not(hasItem(unwantedVal))) + } + } + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteVarPropertyTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteVarPropertyTest.kt new file mode 100644 index 0000000000..392f9466ea --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteVarPropertyTest.kt @@ -0,0 +1,141 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.matcher.* +import net.corda.gradle.unwanted.HasUnwantedVar +import org.hamcrest.core.IsCollectionContaining.* +import org.hamcrest.core.IsNot.* +import org.junit.Assert.* +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import kotlin.reflect.full.declaredMemberProperties +import kotlin.test.assertFailsWith + +class DeleteVarPropertyTest { + companion object { + private const val PROPERTY_CLASS = "net.corda.gradle.HasUnwantedVarPropertyForDelete" + private const val GETTER_CLASS = "net.corda.gradle.HasUnwantedGetForDelete" + private const val SETTER_CLASS = "net.corda.gradle.HasUnwantedSetForDelete" + private const val JVM_FIELD_CLASS = "net.corda.gradle.HasVarJvmFieldForDelete" + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "delete-var-property") + private val unwantedVar = isProperty("unwantedVar", String::class) + private val getUnwantedVar = isMethod("getUnwantedVar", String::class.java) + private val setUnwantedVar = isMethod("setUnwantedVar", Void.TYPE, String::class.java) + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + } + + @Test + fun deleteProperty() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(DEFAULT_MESSAGE).also { obj -> + assertEquals(DEFAULT_MESSAGE, obj.unwantedVar) + obj.unwantedVar = MESSAGE + assertEquals(MESSAGE, obj.unwantedVar) + } + assertFalse(getDeclaredField("unwantedVar").isAccessible) + assertThat("unwantedVar not found", kotlin.declaredMemberProperties, hasItem(unwantedVar)) + assertThat("getUnwantedVar not found", kotlin.javaDeclaredMethods, hasItem(getUnwantedVar)) + assertThat("setUnwantedVar not found", kotlin.javaDeclaredMethods, hasItem(setUnwantedVar)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(DEFAULT_MESSAGE).also { obj -> + assertFailsWith { obj.unwantedVar } + assertFailsWith { obj.unwantedVar = MESSAGE } + } + assertFailsWith { getDeclaredField("unwantedVar") } + assertThat("unwantedVar still exists", kotlin.declaredMemberProperties, not(hasItem(unwantedVar))) + assertThat("getUnwantedVar still exists", kotlin.javaDeclaredMethods, not(hasItem(getUnwantedVar))) + assertThat("setUnwantedVar still exists", kotlin.javaDeclaredMethods, not(hasItem(setUnwantedVar))) + } + } + } + + @Test + fun deleteGetter() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(GETTER_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj -> + assertEquals(MESSAGE, obj.unwantedVar) + } + assertFalse(getDeclaredField("unwantedVar").isAccessible) + assertThat("getUnwantedVar not found", kotlin.javaDeclaredMethods, hasItem(getUnwantedVar)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(GETTER_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj -> + assertFailsWith { obj.unwantedVar } + } + assertFalse(getDeclaredField("unwantedVar").isAccessible) + assertThat("getUnwantedVar still exists", kotlin.javaDeclaredMethods, not(hasItem(getUnwantedVar))) + } + } + } + + @Test + fun deleteSetter() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(SETTER_CLASS).apply { + getConstructor(String::class.java).newInstance(DEFAULT_MESSAGE).also { obj -> + assertEquals(DEFAULT_MESSAGE, obj.unwantedVar) + obj.unwantedVar = MESSAGE + assertEquals(MESSAGE, obj.unwantedVar) + } + getDeclaredField("unwantedVar").also { field -> + assertFalse(field.isAccessible) + } + assertThat("setUnwantedVar not found", kotlin.javaDeclaredMethods, hasItem(setUnwantedVar)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(SETTER_CLASS).apply { + getConstructor(String::class.java).newInstance(DEFAULT_MESSAGE).also { obj -> + assertEquals(DEFAULT_MESSAGE, obj.unwantedVar) + assertFailsWith { obj.unwantedVar = MESSAGE } + } + getDeclaredField("unwantedVar").also { field -> + assertFalse(field.isAccessible) + } + assertThat("setUnwantedVar still exists", kotlin.javaDeclaredMethods, not(hasItem(setUnwantedVar))) + } + } + } + + @Test + fun deleteJvmField() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(JVM_FIELD_CLASS).apply { + val obj: Any = getDeclaredConstructor(String::class.java).newInstance(DEFAULT_MESSAGE) + getDeclaredField("unwantedVar").also { field -> + assertEquals(DEFAULT_MESSAGE, field.get(obj)) + field.set(obj, MESSAGE) + assertEquals(MESSAGE, field.get(obj)) + } + assertThat("unwantedVar not found", kotlin.declaredMemberProperties, hasItem(unwantedVar)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(JVM_FIELD_CLASS).apply { + assertNotNull(getDeclaredConstructor(String::class.java).newInstance(DEFAULT_MESSAGE)) + assertFailsWith { getDeclaredField("unwantedVar") } + assertThat("unwantedVar still exists", kotlin.declaredMemberProperties, not(hasItem(unwantedVar))) + } + } + } +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DummyJar.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DummyJar.kt new file mode 100644 index 0000000000..c332860669 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DummyJar.kt @@ -0,0 +1,104 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.asm.bytecode +import net.corda.gradle.jarfilter.asm.resourceName +import org.assertj.core.api.Assertions.* +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import java.nio.file.Files +import java.nio.file.Path +import java.util.jar.Attributes.Name.MANIFEST_VERSION +import java.util.jar.JarOutputStream +import java.util.jar.Manifest +import java.util.zip.CRC32 +import java.util.zip.Deflater.NO_COMPRESSION +import java.util.zip.ZipEntry +import java.util.zip.ZipEntry.* + +/** + * Creates a dummy jar containing the following: + * - META-INF/MANIFEST.MF + * - A compressed class file + * - A compressed binary non-class file + * - An uncompressed text file + * - A directory entry + * + * The compression level is set to NO_COMPRESSION + * in order to force the Gradle task to compress + * the entries properly. + */ +class DummyJar( + private val projectDir: TemporaryFolder, + private val testClass: Class<*>, + private val name: String +) : TestRule { + private companion object { + private const val DATA_SIZE = 512 + + private fun uncompressed(name: String, data: ByteArray) = ZipEntry(name).apply { + method = STORED + compressedSize = data.size.toLong() + size = data.size.toLong() + crc = CRC32().let { crc -> + crc.update(data) + crc.value + } + } + + private fun compressed(name: String) = ZipEntry(name).apply { method = DEFLATED } + + private fun directoryOf(type: Class<*>) + = directory(type.`package`.name.toPathFormat + '/') + + private fun directory(name: String) = ZipEntry(name).apply { + method = STORED + compressedSize = 0 + size = 0 + crc = 0 + } + } + + private lateinit var _path: Path + val path: Path get() = _path + + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + val manifest = Manifest().apply { + mainAttributes.also { main -> + main[MANIFEST_VERSION] = "1.0" + } + } + _path = projectDir.pathOf("$name.jar") + JarOutputStream(Files.newOutputStream(_path), manifest).use { jar -> + jar.setComment(testClass.name) + jar.setLevel(NO_COMPRESSION) + + // One directory entry (stored) + jar.putNextEntry(directoryOf(testClass)) + + // One compressed class file + jar.putNextEntry(compressed(testClass.resourceName)) + jar.write(testClass.bytecode) + + // One compressed non-class file + jar.putNextEntry(compressed("binary.dat")) + jar.write(arrayOfJunk(DATA_SIZE)) + + // One uncompressed text file + val text = """ +Jar: ${_path.toAbsolutePath()} +Class: ${testClass.name} +""".toByteArray() + jar.putNextEntry(uncompressed("comment.txt", text)) + jar.write(text) + } + assertThat(_path).isRegularFile() + + base.evaluate() + } + } + } +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/EmptyPackage.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/EmptyPackage.kt new file mode 100644 index 0000000000..bbb76a8036 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/EmptyPackage.kt @@ -0,0 +1,8 @@ +@file:JvmName("EmptyPackage") +@file:Suppress("UNUSED") +package net.corda.gradle.jarfilter + +/* + * We need to put something in here so that Kotlin will create a class file. + */ +const val PLACEHOLDER = 0 diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/FieldElementTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/FieldElementTest.kt new file mode 100644 index 0000000000..0d54ecba27 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/FieldElementTest.kt @@ -0,0 +1,32 @@ +package net.corda.gradle.jarfilter + +import org.junit.Assert.* +import org.junit.Test + +class FieldElementTest { + private companion object { + private const val DESCRIPTOR = "Ljava.lang.String;" + } + + @Test + fun testFieldsMatchByNameOnly() { + val elt = FieldElement(name = "fieldName", descriptor = DESCRIPTOR) + assertEquals(FieldElement(name = "fieldName"), elt) + } + + @Test + fun testFieldWithDescriptorDoesNotExpire() { + val elt = FieldElement(name = "fieldName", descriptor = DESCRIPTOR) + assertFalse(elt.isExpired) + assertFalse(elt.isExpired) + assertFalse(elt.isExpired) + } + + @Test + fun testFieldWithoutDescriptorDoesExpire() { + val elt = FieldElement(name = "fieldName") + assertFalse(elt.isExpired) + assertTrue(elt.isExpired) + assertTrue(elt.isExpired) + } +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/FieldRemovalTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/FieldRemovalTest.kt new file mode 100644 index 0000000000..da80e93660 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/FieldRemovalTest.kt @@ -0,0 +1,212 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.annotations.Deletable +import net.corda.gradle.jarfilter.asm.bytecode +import net.corda.gradle.jarfilter.asm.toClass +import net.corda.gradle.jarfilter.matcher.isProperty +import org.gradle.api.logging.Logger +import org.hamcrest.core.IsCollectionContaining.hasItem +import org.hamcrest.core.IsEqual.equalTo +import org.hamcrest.core.IsNot.not +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertThat +import org.junit.Test +import org.objectweb.asm.ClassWriter.COMPUTE_MAXS +import kotlin.reflect.full.declaredMemberProperties +import kotlin.reflect.jvm.jvmName +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +/** + * Demonstrate that we can still instantiate objects, even after we've deleted + * one of their properties. (Check we haven't blown the constructor away!) + */ +class FieldRemovalTest { + companion object { + private val logger: Logger = StdOutLogging(FieldRemovalTest::class) + private const val SHORT_NUMBER = 999.toShort() + private const val BYTE_NUMBER = 99.toByte() + private const val BIG_FLOATING_POINT = 9999999.9999 + private const val FLOATING_POINT = 9999.99f + + private val objectField = isProperty(equalTo("objectField"), equalTo("T")) + private val longField = isProperty("longField", Long::class) + private val intField = isProperty("intField", Int::class) + private val shortField = isProperty("shortField", Short::class) + private val byteField = isProperty("byteField", Byte::class) + private val charField = isProperty("charField", Char::class) + private val booleanField = isProperty("booleanField", Boolean::class) + private val doubleField = isProperty("doubleField", Double::class) + private val floatField = isProperty("floatField", Float::class) + private val arrayField = isProperty("arrayField", ByteArray::class) + } + + private inline fun transform(): Class = transform(T::class.java, R::class.java) + + private fun transform(type: Class, asType: Class): Class { + val bytecode = type.bytecode.execute({ writer -> + ClassTransformer( + visitor = writer, + logger = logger, + removeAnnotations = emptySet(), + deleteAnnotations = setOf(Deletable::class.jvmName.descriptor), + stubAnnotations = emptySet(), + unwantedClasses = mutableSetOf() + ) + }, COMPUTE_MAXS) + return bytecode.toClass(type, asType) + } + + @Test + fun removeObject() { + val sourceField = SampleGenericField(MESSAGE) + assertEquals(MESSAGE, sourceField.objectField) + assertThat("objectField not found", sourceField::class.declaredMemberProperties, hasItem(objectField)) + + val targetField = transform, HasGenericField>() + .getDeclaredConstructor(Any::class.java).newInstance(MESSAGE) + assertFailsWith { targetField.objectField } + assertFailsWith { targetField.objectField = "New Value" } + assertThat("objectField still exists", targetField::class.declaredMemberProperties, not(hasItem(objectField))) + } + + @Test + fun removeLong() { + val sourceField = SampleLongField(BIG_NUMBER) + assertEquals(BIG_NUMBER, sourceField.longField) + assertThat("longField not found", sourceField::class.declaredMemberProperties, hasItem(longField)) + + val targetField = transform() + .getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER) + assertFailsWith { targetField.longField } + assertFailsWith { targetField.longField = 10L } + assertThat("longField still exists", targetField::class.declaredMemberProperties, not(hasItem(longField))) + } + + @Test + fun removeInt() { + val sourceField = SampleIntField(NUMBER) + assertEquals(NUMBER, sourceField.intField) + assertThat("intField not found", sourceField::class.declaredMemberProperties, hasItem(intField)) + + val targetField = transform() + .getDeclaredConstructor(Int::class.java).newInstance(NUMBER) + assertFailsWith { targetField.intField } + assertFailsWith { targetField.intField = 100 } + assertThat("intField still exists", targetField::class.declaredMemberProperties, not(hasItem(intField))) + } + + @Test + fun removeShort() { + val sourceField = SampleShortField(SHORT_NUMBER) + assertEquals(SHORT_NUMBER, sourceField.shortField) + assertThat("shortField not found", sourceField::class.declaredMemberProperties, hasItem(shortField)) + + val targetField = transform() + .getDeclaredConstructor(Short::class.java).newInstance(SHORT_NUMBER) + assertFailsWith { targetField.shortField } + assertFailsWith { targetField.shortField = 15 } + assertThat("shortField still exists", targetField::class.declaredMemberProperties, not(hasItem(shortField))) + } + + @Test + fun removeByte() { + val sourceField = SampleByteField(BYTE_NUMBER) + assertEquals(BYTE_NUMBER, sourceField.byteField) + assertThat("byteField not found", sourceField::class.declaredMemberProperties, hasItem(byteField)) + + val targetField = transform() + .getDeclaredConstructor(Byte::class.java).newInstance(BYTE_NUMBER) + assertFailsWith { targetField.byteField } + assertFailsWith { targetField.byteField = 16 } + assertThat("byteField still exists", targetField::class.declaredMemberProperties, not(hasItem(byteField))) + } + + @Test + fun removeBoolean() { + val sourceField = SampleBooleanField(true) + assertTrue(sourceField.booleanField) + assertThat("booleanField not found", sourceField::class.declaredMemberProperties, hasItem(booleanField)) + + val targetField = transform() + .getDeclaredConstructor(Boolean::class.java).newInstance(true) + assertFailsWith { targetField.booleanField } + assertFailsWith { targetField.booleanField = false } + assertThat("booleanField still exists", targetField::class.declaredMemberProperties, not(hasItem(booleanField))) + } + + @Test + fun removeChar() { + val sourceField = SampleCharField('?') + assertEquals('?', sourceField.charField) + assertThat("charField not found", sourceField::class.declaredMemberProperties, hasItem(charField)) + + val targetField = transform() + .getDeclaredConstructor(Char::class.java).newInstance('?') + assertFailsWith { targetField.charField } + assertFailsWith { targetField.charField = 'A' } + assertThat("charField still exists", targetField::class.declaredMemberProperties, not(hasItem(charField))) + } + + @Test + fun removeDouble() { + val sourceField = SampleDoubleField(BIG_FLOATING_POINT) + assertEquals(BIG_FLOATING_POINT, sourceField.doubleField) + assertThat("doubleField not found", sourceField::class.declaredMemberProperties, hasItem(doubleField)) + + val targetField = transform() + .getDeclaredConstructor(Double::class.java).newInstance(BIG_FLOATING_POINT) + assertFailsWith { targetField.doubleField } + assertFailsWith { targetField.doubleField = 12345.678 } + assertThat("doubleField still exists", targetField::class.declaredMemberProperties, not(hasItem(doubleField))) + } + + @Test + fun removeFloat() { + val sourceField = SampleFloatField(FLOATING_POINT) + assertEquals(FLOATING_POINT, sourceField.floatField) + assertThat("floatField not found", sourceField::class.declaredMemberProperties, hasItem(floatField)) + + val targetField = transform() + .getDeclaredConstructor(Float::class.java).newInstance(FLOATING_POINT) + assertFailsWith { targetField.floatField } + assertFailsWith { targetField.floatField = 123.45f } + assertThat("floatField still exists", targetField::class.declaredMemberProperties, not(hasItem(floatField))) + } + + @Test + fun removeArray() { + val sourceField = SampleArrayField(byteArrayOf()) + assertArrayEquals(byteArrayOf(), sourceField.arrayField) + assertThat("arrayField not found", sourceField::class.declaredMemberProperties, hasItem(arrayField)) + + val targetField = transform() + .getDeclaredConstructor(ByteArray::class.java).newInstance(byteArrayOf()) + assertFailsWith { targetField.arrayField } + assertFailsWith { targetField.arrayField = byteArrayOf(0x35, 0x73) } + assertThat("arrayField still exists", targetField::class.declaredMemberProperties, not(hasItem(arrayField))) + } +} + +interface HasGenericField { var objectField: T } +interface HasLongField { var longField: Long } +interface HasIntField { var intField: Int } +interface HasShortField { var shortField: Short } +interface HasByteField { var byteField: Byte } +interface HasBooleanField { var booleanField: Boolean } +interface HasCharField { var charField: Char } +interface HasFloatField { var floatField: Float } +interface HasDoubleField { var doubleField: Double } +interface HasArrayField { var arrayField: ByteArray } + +internal class SampleGenericField(@Deletable override var objectField: T) : HasGenericField +internal class SampleLongField(@Deletable override var longField: Long) : HasLongField +internal class SampleIntField(@Deletable override var intField: Int) : HasIntField +internal class SampleShortField(@Deletable override var shortField: Short) : HasShortField +internal class SampleByteField(@Deletable override var byteField: Byte) : HasByteField +internal class SampleBooleanField(@Deletable override var booleanField: Boolean) : HasBooleanField +internal class SampleCharField(@Deletable override var charField: Char) : HasCharField +internal class SampleFloatField(@Deletable override var floatField: Float) : HasFloatField +internal class SampleDoubleField(@Deletable override var doubleField: Double) : HasDoubleField +internal class SampleArrayField(@Deletable override var arrayField: ByteArray) : HasArrayField diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/InterfaceFunctionTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/InterfaceFunctionTest.kt new file mode 100644 index 0000000000..452f26d140 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/InterfaceFunctionTest.kt @@ -0,0 +1,61 @@ +package net.corda.gradle.jarfilter + +import org.junit.Assert.* +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import java.lang.reflect.Modifier.* +import kotlin.test.assertFailsWith + +class InterfaceFunctionTest { + companion object { + private const val FUNCTION_CLASS = "net.corda.gradle.InterfaceFunctions" + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "interface-function") + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + } + + @Test + fun deleteInterfaceFunction() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + getMethod("toDelete", Long::class.java).also { method -> + assertEquals(ABSTRACT, method.modifiers and ABSTRACT) + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + assertFailsWith { getMethod("toDelete", Long::class.java) } + } + } + } + + @Test + fun cannotStubInterfaceFunction() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + getMethod("toStubOut", Long::class.java).also { method -> + assertEquals(ABSTRACT, method.modifiers and ABSTRACT) + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + getMethod("toStubOut", Long::class.java).also { method -> + assertEquals(ABSTRACT, method.modifiers and ABSTRACT) + } + } + } + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/JarFilterConfigurationTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/JarFilterConfigurationTest.kt new file mode 100644 index 0000000000..a124545ae4 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/JarFilterConfigurationTest.kt @@ -0,0 +1,272 @@ +package net.corda.gradle.jarfilter + +import org.assertj.core.api.Assertions.* +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.BuildTask +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome.* +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class JarFilterConfigurationTest { + private companion object { + private const val AMBIGUOUS = "net.corda.gradle.jarfilter.Ambiguous" + private const val DELETE = "net.corda.gradle.jarfilter.DeleteMe" + private const val REMOVE = "net.corda.gradle.jarfilter.RemoveMe" + private const val STUB = "net.corda.gradle.jarfilter.StubMeOut" + } + + @Rule + @JvmField + val testProjectDir = TemporaryFolder() + + private lateinit var output: String + + @Before + fun setup() { + testProjectDir.installResource("gradle.properties") + } + + @Test + fun checkNoJarMeansNoSource() { + val result = gradleProject(""" +plugins { + id 'java' + id 'net.corda.plugins.jar-filter' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + annotations { + forDelete = ["$DELETE"] + } +} +""").build() + output = result.output + println(output) + + val jarFilter = result.forTask("jarFilter") + assertEquals(NO_SOURCE, jarFilter.outcome) + } + + @Test + fun checkWithMissingJar() { + val result = gradleProject(""" +plugins { + id 'net.corda.plugins.jar-filter' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars = file('does-not-exist.jar') +} +""").buildAndFail() + output = result.output + println(output) + + assertThat(output).containsSubsequence( + "Caused by: org.gradle.api.GradleException:", + "Caused by: java.io.FileNotFoundException:" + ) + + val jarFilter = result.forTask("jarFilter") + assertEquals(FAILED, jarFilter.outcome) + } + + @Test + fun checkSameAnnotationForRemoveAndDelete() { + val result = gradleProject(""" +plugins { + id 'java' + id 'net.corda.plugins.jar-filter' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars = jar + annotations { + forDelete = ["$AMBIGUOUS"] + forRemove = ["$AMBIGUOUS"] + } +} +""").buildAndFail() + output = result.output + println(output) + + assertThat(output).containsSequence( + "Caused by: org.gradle.api.InvalidUserDataException: Annotation 'net.corda.gradle.jarfilter.Ambiguous' also appears in JarFilter 'forDelete' section" + ) + + val jarFilter = result.forTask("jarFilter") + assertEquals(FAILED, jarFilter.outcome) + } + + @Test + fun checkSameAnnotationForRemoveAndStub() { + val result = gradleProject(""" +plugins { + id 'java' + id 'net.corda.plugins.jar-filter' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars = jar + annotations { + forStub = ["$AMBIGUOUS"] + forRemove = ["$AMBIGUOUS"] + } +} +""").buildAndFail() + output = result.output + println(output) + + assertThat(output).containsSequence( + "Caused by: org.gradle.api.InvalidUserDataException: Annotation 'net.corda.gradle.jarfilter.Ambiguous' also appears in JarFilter 'forStub' section" + ) + + val jarFilter = result.forTask("jarFilter") + assertEquals(FAILED, jarFilter.outcome) + } + + @Test + fun checkSameAnnotationForStubAndDelete() { + val result = gradleProject(""" +plugins { + id 'java' + id 'net.corda.plugins.jar-filter' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars = jar + annotations { + forStub = ["$AMBIGUOUS"] + forDelete = ["$AMBIGUOUS"] + } +} +""").buildAndFail() + output = result.output + println(output) + + assertThat(output).containsSequence( + "Caused by: org.gradle.api.InvalidUserDataException: Annotation 'net.corda.gradle.jarfilter.Ambiguous' also appears in JarFilter 'forStub' section" + ) + + val jarFilter = result.forTask("jarFilter") + assertEquals(FAILED, jarFilter.outcome) + } + + @Test + fun checkSameAnnotationForStubAndDeleteAndRemove() { + val result = gradleProject(""" +plugins { + id 'java' + id 'net.corda.plugins.jar-filter' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars = jar + annotations { + forStub = ["$AMBIGUOUS"] + forDelete = ["$AMBIGUOUS"] + forRemove = ["$AMBIGUOUS"] + } +} +""").buildAndFail() + output = result.output + println(output) + + assertThat(output).containsSequence( + "Caused by: org.gradle.api.InvalidUserDataException: Annotation 'net.corda.gradle.jarfilter.Ambiguous' also appears in JarFilter 'forDelete' section" + ) + + val jarFilter = result.forTask("jarFilter") + assertEquals(FAILED, jarFilter.outcome) + } + + @Test + fun checkRepeatedAnnotationForDelete() { + val result = gradleProject(""" +plugins { + id 'java' + id 'net.corda.plugins.jar-filter' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars = jar + annotations { + forDelete = ["$DELETE", "$DELETE"] + } +} +""").build() + output = result.output + println(output) + + val jarFilter = result.forTask("jarFilter") + assertEquals(SUCCESS, jarFilter.outcome) + } + + @Test + fun checkRepeatedAnnotationForStub() { + val result = gradleProject(""" +plugins { + id 'java' + id 'net.corda.plugins.jar-filter' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars = jar + annotations { + forStub = ["$STUB", "$STUB"] + } +} +""").build() + output = result.output + println(output) + + val jarFilter = result.forTask("jarFilter") + assertEquals(SUCCESS, jarFilter.outcome) + } + + @Test + fun checkRepeatedAnnotationForRemove() { + val result = gradleProject(""" +plugins { + id 'java' + id 'net.corda.plugins.jar-filter' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars = jar + annotations { + forRemove = ["$REMOVE", "$REMOVE"] + } +} +""").build() + output = result.output + println(output) + + val jarFilter = result.forTask("jarFilter") + assertEquals(SUCCESS, jarFilter.outcome) + } + + private fun gradleProject(script: String): GradleRunner { + testProjectDir.newFile("build.gradle").writeText(script) + return GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments(getBasicArgsForTasks("jarFilter", "--stacktrace")) + .withPluginClasspath() + } + + private fun BuildResult.forTask(name: String): BuildTask { + return task(":$name") ?: throw AssertionError("No outcome for $name task") + } +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/JarFilterProject.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/JarFilterProject.kt new file mode 100644 index 0000000000..1a2e3cb128 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/JarFilterProject.kt @@ -0,0 +1,56 @@ +package net.corda.gradle.jarfilter + +import org.assertj.core.api.Assertions.* +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome.* +import org.junit.Assert.* +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import java.io.FileNotFoundException +import java.nio.file.Path + +class JarFilterProject(private val projectDir: TemporaryFolder, private val name: String) : TestRule { + private var _sourceJar: Path? = null + val sourceJar: Path get() = _sourceJar ?: throw FileNotFoundException("Input not found") + + private var _filteredJar: Path? = null + val filteredJar: Path get() = _filteredJar ?: throw FileNotFoundException("Output not found") + + private var _output: String = "" + val output: String get() = _output + + override fun apply(statement: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + projectDir.installResources( + "$name/build.gradle", + "repositories.gradle", + "gradle.properties", + "settings.gradle" + ) + + val result = GradleRunner.create() + .withProjectDir(projectDir.root) + .withArguments(getGradleArgsForTasks("jarFilter")) + .withPluginClasspath() + .build() + _output = result.output + println(output) + + val jarFilter = result.task(":jarFilter") + ?: throw AssertionError("No outcome for jarFilter task") + assertEquals(SUCCESS, jarFilter.outcome) + + _sourceJar = projectDir.pathOf("build", "libs", "$name.jar") + assertThat(sourceJar).isRegularFile() + + _filteredJar = projectDir.pathOf("build", "filtered-libs", "$name-filtered.jar") + assertThat(filteredJar).isRegularFile() + + statement.evaluate() + } + } + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/JarFilterTimestampTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/JarFilterTimestampTest.kt new file mode 100644 index 0000000000..d57aca4b65 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/JarFilterTimestampTest.kt @@ -0,0 +1,107 @@ +package net.corda.gradle.jarfilter + +import org.assertj.core.api.Assertions.* +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome.* +import org.junit.Assert.* +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import org.junit.runners.model.Statement +import java.nio.file.Path +import java.nio.file.attribute.FileTime +import java.util.* +import java.util.Calendar.FEBRUARY +import java.util.zip.ZipEntry +import java.util.zip.ZipFile + +class JarFilterTimestampTest { + companion object { + private val testProjectDir = TemporaryFolder() + private val sourceJar = DummyJar(testProjectDir, JarFilterTimestampTest::class.java, "timestamps") + + private val CONSTANT_TIME: FileTime = FileTime.fromMillis( + GregorianCalendar(1980, FEBRUARY, 1).apply { + timeZone = TimeZone.getTimeZone("UTC") + }.timeInMillis + ) + + private lateinit var filteredJar: Path + private lateinit var output: String + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(sourceJar) + .around(createTestProject()) + + private fun createTestProject() = TestRule { base, _ -> + object : Statement() { + override fun evaluate() { + testProjectDir.installResource("gradle.properties") + testProjectDir.newFile("build.gradle").writeText(""" +plugins { + id 'net.corda.plugins.jar-filter' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars file("${sourceJar.path.toUri()}") + preserveTimestamps = false +} +""") + val result = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments(getGradleArgsForTasks("jarFilter")) + .withPluginClasspath() + .build() + output = result.output + println(output) + + val metafix = result.task(":jarFilter") + ?: throw AssertionError("No outcome for jarFilter task") + assertEquals(SUCCESS, metafix.outcome) + + filteredJar = testProjectDir.pathOf("build", "filtered-libs", "timestamps-filtered.jar") + assertThat(filteredJar).isRegularFile() + + base.evaluate() + } + } + } + + private val ZipEntry.methodName: String get() = if (method == ZipEntry.STORED) "Stored" else "Deflated" + } + + @Test + fun fileTimestampsAreRemoved() { + var directoryCount = 0 + var classCount = 0 + var otherCount = 0 + + ZipFile(filteredJar.toFile()).use { jar -> + for (entry in jar.entries()) { + println("Entry: ${entry.name}") + println("- ${entry.methodName} (${entry.size} size / ${entry.compressedSize} compressed) bytes") + assertThat(entry.lastModifiedTime).isEqualTo(CONSTANT_TIME) + assertThat(entry.lastAccessTime).isNull() + assertThat(entry.creationTime).isNull() + + if (entry.isDirectory) { + ++directoryCount + } else if (entry.name.endsWith(".class")) { + ++classCount + } else { + ++otherCount + } + } + } + + assertThat(directoryCount).isGreaterThan(0) + assertThat(classCount).isGreaterThan(0) + assertThat(otherCount).isGreaterThan(0) + } +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixAnnotationTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixAnnotationTest.kt new file mode 100644 index 0000000000..1fdcc44e7e --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixAnnotationTest.kt @@ -0,0 +1,47 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.asm.bytecode +import net.corda.gradle.jarfilter.asm.toClass +import net.corda.gradle.jarfilter.matcher.isConstructor +import org.gradle.api.logging.Logger +import org.hamcrest.core.IsCollectionContaining.* +import org.junit.Assert.* +import org.junit.Test + +class MetaFixAnnotationTest { + companion object { + private val logger: Logger = StdOutLogging(MetaFixAnnotationTest::class) + private val defaultCon = isConstructor( + returnType = SimpleAnnotation::class + ) + private val valueCon = isConstructor( + returnType = AnnotationWithValue::class, + parameters = *arrayOf(String::class) + ) + } + + @Test + fun testSimpleAnnotation() { + val sourceClass = SimpleAnnotation::class.java + assertThat("() not found", sourceClass.kotlin.constructors, hasItem(defaultCon)) + + // Rewrite the metadata according to the contents of the bytecode. + val fixedClass = sourceClass.bytecode.fixMetadata(logger, pathsOf(SimpleAnnotation::class)) + .toClass() + assertThat("() not found", fixedClass.kotlin.constructors, hasItem(defaultCon)) + } + + @Test + fun testAnnotationWithValue() { + val sourceClass = AnnotationWithValue::class.java + assertThat("(String) not found", sourceClass.kotlin.constructors, hasItem(valueCon)) + + // Rewrite the metadata according to the contents of the bytecode. + val fixedClass = sourceClass.bytecode.fixMetadata(logger, pathsOf(AnnotationWithValue::class)) + .toClass() + assertThat("(String) not found", fixedClass.kotlin.constructors, hasItem(valueCon)) + } +} + +annotation class AnnotationWithValue(val str: String) +annotation class SimpleAnnotation diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixConfigurationTests.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixConfigurationTests.kt new file mode 100644 index 0000000000..bb67dba0c2 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixConfigurationTests.kt @@ -0,0 +1,79 @@ +package net.corda.gradle.jarfilter + +import org.assertj.core.api.Assertions.* +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.BuildTask +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome.* +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class MetaFixConfigurationTests { + @Rule + @JvmField + val testProjectDir = TemporaryFolder() + + private lateinit var output: String + + @Before + fun setup() { + testProjectDir.installResource("gradle.properties") + } + + @Test + fun checkNoJarMeansNoSource() { + val result = gradleProject(""" +plugins { + id 'java' + id 'net.corda.plugins.jar-filter' +} + +import net.corda.gradle.jarfilter.MetaFixerTask +task metafix(type: MetaFixerTask) +""").build() + output = result.output + println(output) + + val metafix = result.forTask("metafix") + assertEquals(NO_SOURCE, metafix.outcome) + } + + @Test + fun checkWithMissingJar() { + val result = gradleProject(""" +plugins { + id 'net.corda.plugins.jar-filter' +} + +import net.corda.gradle.jarfilter.MetaFixerTask +task metafix(type: MetaFixerTask) { + jars = file('does-not-exist.jar') +} +""").buildAndFail() + output = result.output + println(output) + + assertThat(output).containsSubsequence( + "Caused by: org.gradle.api.GradleException:", + "Caused by: java.io.FileNotFoundException:" + ) + + val metafix = result.forTask("metafix") + assertEquals(FAILED, metafix.outcome) + } + + private fun gradleProject(script: String): GradleRunner { + testProjectDir.newFile("build.gradle").writeText(script) + return GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments(getBasicArgsForTasks("metafix", "--stacktrace")) + .withPluginClasspath() + } + + private fun BuildResult.forTask(name: String): BuildTask { + return task(":$name") ?: throw AssertionError("No outcome for $name task") + } +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixConstructorTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixConstructorTest.kt new file mode 100644 index 0000000000..cbab25571a --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixConstructorTest.kt @@ -0,0 +1,55 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.unwanted.* +import net.corda.gradle.jarfilter.asm.* +import net.corda.gradle.jarfilter.matcher.* +import org.gradle.api.logging.Logger +import org.hamcrest.core.IsCollectionContaining.hasItem +import org.hamcrest.core.IsNot.not +import org.junit.Assert.* +import org.junit.Test +import kotlin.jvm.kotlin + +class MetaFixConstructorTest { + companion object { + private val logger: Logger = StdOutLogging(MetaFixConstructorTest::class) + private val unwantedCon = isConstructor( + returnType = WithConstructor::class, + parameters = *arrayOf(Int::class, Long::class) + ) + private val wantedCon = isConstructor( + returnType = WithConstructor::class, + parameters = *arrayOf(Long::class) + ) + } + + @Test + fun testConstructorRemovedFromMetadata() { + val bytecode = recodeMetadataFor() + val sourceClass = bytecode.toClass() + + // Check that the unwanted constructor has been successfully + // added to the metadata, and that the class is valid. + val sourceObj = sourceClass.getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER) + assertEquals(BIG_NUMBER, sourceObj.longData()) + assertThat("(Int,Long) not found", sourceClass.kotlin.constructors, hasItem(unwantedCon)) + assertThat("(Long) not found", sourceClass.kotlin.constructors, hasItem(wantedCon)) + + // Rewrite the metadata according to the contents of the bytecode. + val fixedClass = bytecode.fixMetadata(logger, pathsOf(WithConstructor::class)).toClass() + val fixedObj = fixedClass.getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER) + assertEquals(BIG_NUMBER, fixedObj.longData()) + assertThat("(Int,Long) still exists", fixedClass.kotlin.constructors, not(hasItem(unwantedCon))) + assertThat("(Long) not found", fixedClass.kotlin.constructors, hasItem(wantedCon)) + } + + class MetadataTemplate(private val longData: Long) : HasLong { + @Suppress("UNUSED_PARAMETER", "UNUSED") + constructor(intData: Int, longData: Long) : this(longData) + override fun longData(): Long = longData + } +} + +class WithConstructor(private val longData: Long) : HasLong { + override fun longData(): Long = longData +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixFunctionTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixFunctionTest.kt new file mode 100644 index 0000000000..1f4011d84e --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixFunctionTest.kt @@ -0,0 +1,53 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.unwanted.* +import net.corda.gradle.jarfilter.asm.* +import net.corda.gradle.jarfilter.matcher.* +import org.gradle.api.logging.Logger +import org.hamcrest.core.IsCollectionContaining.hasItem +import org.hamcrest.core.IsNot.not +import org.junit.Assert.* +import org.junit.Test +import kotlin.jvm.kotlin +import kotlin.reflect.full.declaredFunctions + +class MetaFixFunctionTest { + companion object { + private val logger: Logger = StdOutLogging(MetaFixFunctionTest::class) + private val longData = isFunction("longData", Long::class) + private val unwantedFun = isFunction( + name = "unwantedFun", + returnType = String::class, + parameters = *arrayOf(String::class) + ) + } + + @Test + fun testFunctionRemovedFromMetadata() { + val bytecode = recodeMetadataFor() + val sourceClass = bytecode.toClass() + + // Check that the unwanted function has been successfully + // added to the metadata, and that the class is valid. + val sourceObj = sourceClass.newInstance() + assertEquals(BIG_NUMBER, sourceObj.longData()) + assertThat("unwantedFun(String) not found", sourceClass.kotlin.declaredFunctions, hasItem(unwantedFun)) + assertThat("longData not found", sourceClass.kotlin.declaredFunctions, hasItem(longData)) + + // Rewrite the metadata according to the contents of the bytecode. + val fixedClass = bytecode.fixMetadata(logger, pathsOf(WithFunction::class)).toClass() + val fixedObj = fixedClass.newInstance() + assertEquals(BIG_NUMBER, fixedObj.longData()) + assertThat("unwantedFun(String) still exists", fixedClass.kotlin.declaredFunctions, not(hasItem(unwantedFun))) + assertThat("longData not found", fixedClass.kotlin.declaredFunctions, hasItem(longData)) + } + + class MetadataTemplate : HasLong { + override fun longData(): Long = 0 + @Suppress("UNUSED") fun unwantedFun(str: String): String = "UNWANTED[$str]" + } +} + +class WithFunction : HasLong { + override fun longData(): Long = BIG_NUMBER +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixNestedClassTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixNestedClassTest.kt new file mode 100644 index 0000000000..d460e9c453 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixNestedClassTest.kt @@ -0,0 +1,57 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.asm.* +import org.assertj.core.api.Assertions.* +import org.gradle.api.logging.Logger +import org.junit.Test +import kotlin.reflect.jvm.jvmName + +/** + * Kotlin reflection will attempt to validate the nested classes stored in the [kotlin.Metadata] + * annotation rather than just reporting what is there, which means that it can tell us nothing + * about what the MetaFixer task has done. + */ +class MetaFixNestedClassTest { + companion object { + private val logger: Logger = StdOutLogging(MetaFixNestedClassTest::class) + private val WANTED_CLASS: String = WithNestedClass.Wanted::class.jvmName + private val UNWANTED_CLASS: String = "${WithNestedClass::class.jvmName}\$Unwanted" + } + + @Test + fun testNestedClassRemovedFromMetadata() { + val bytecode = recodeMetadataFor() + val sourceClass = bytecode.toClass() + assertThat(sourceClass.classMetadata.nestedClasses).containsExactlyInAnyOrder(WANTED_CLASS, UNWANTED_CLASS) + + // Rewrite the metadata according to the contents of the bytecode. + val fixedClass = bytecode.fixMetadata(logger, pathsOf(WithNestedClass::class, WithNestedClass.Wanted::class)) + .toClass() + assertThat(fixedClass.classMetadata.nestedClasses).containsExactly(WANTED_CLASS) + } + + @Test + fun testAllNestedClassesRemovedFromMetadata() { + val bytecode = recodeMetadataFor() + val sourceClass = bytecode.toClass() + assertThat(sourceClass.classMetadata.nestedClasses) + .containsExactlyInAnyOrder("${WithoutNestedClass::class.jvmName}\$Wanted", "${WithoutNestedClass::class.jvmName}\$Unwanted") + + // Rewrite the metadata according to the contents of the bytecode. + val fixedClass = bytecode.fixMetadata(logger, pathsOf(WithoutNestedClass::class)) + .toClass() + assertThat(fixedClass.classMetadata.nestedClasses).isEmpty() + } + + @Suppress("UNUSED") + class MetadataTemplate { + class Wanted + class Unwanted + } +} + +class WithNestedClass { + class Wanted +} + +class WithoutNestedClass \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixPackageTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixPackageTest.kt new file mode 100644 index 0000000000..0ef49ac46b --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixPackageTest.kt @@ -0,0 +1,66 @@ +@file:JvmName("PackageTemplate") +@file:Suppress("UNUSED") +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.asm.* +import net.corda.gradle.jarfilter.matcher.* +import org.gradle.api.logging.Logger +import org.junit.BeforeClass +import org.junit.Test +import kotlin.jvm.kotlin +import kotlin.reflect.full.declaredFunctions +import kotlin.reflect.full.declaredMembers +import kotlin.test.assertFailsWith + +/** + * These tests cannot actually "test" anything until Kotlin reflection + * supports package metadata. Until then, we can only execute the code + * paths to ensure they don't throw any exceptions. + */ +class MetaFixPackageTest { + companion object { + private const val TEMPLATE_CLASS = "net.corda.gradle.jarfilter.PackageTemplate" + private const val EMPTY_CLASS = "net.corda.gradle.jarfilter.EmptyPackage" + private val logger: Logger = StdOutLogging(MetaFixPackageTest::class) + private val staticVal = isProperty("templateVal", Long::class) + private val staticVar = isProperty("templateVar", Int::class) + private val staticFun = isFunction("templateFun", String::class) + + private lateinit var sourceClass: Class + private lateinit var fixedClass: Class + + @BeforeClass + @JvmStatic + fun setup() { + val emptyClass = Class.forName(EMPTY_CLASS) + val bytecode = emptyClass.metadataAs(Class.forName(TEMPLATE_CLASS)) + sourceClass = bytecode.toClass(emptyClass, Any::class.java) + fixedClass = bytecode.fixMetadata(logger, setOf(EMPTY_CLASS)).toClass(sourceClass, Any::class.java) + } + } + + @Test + fun testPackageFunction() { + assertFailsWith { sourceClass.kotlin.declaredFunctions } + //assertThat("templateFun() not found", sourceClass.kotlin.declaredFunctions, hasItem(staticFun)) + //assertThat("templateFun() still exists", fixedClass.kotlin.declaredFunctions, not(hasItem(staticFun))) + } + + @Test + fun testPackageVal() { + assertFailsWith { sourceClass.kotlin.declaredMembers } + //assertThat("templateVal not found", sourceClass.kotlin.declaredMembers, hasItem(staticVal)) + //assertThat("templateVal still exists", fixedClass.kotlin.declaredMembers, not(hasItem(staticVal))) + } + + @Test + fun testPackageVar() { + assertFailsWith { sourceClass.kotlin.declaredMembers } + //assertThat("templateVar not found", sourceClass.kotlin.declaredMembers, hasItem(staticVar)) + //assertThat("templateVar still exists", fixedClass.kotlin.declaredMembers, not(hasItem(staticVar))) + } +} + +internal fun templateFun(): String = MESSAGE +internal const val templateVal: Long = BIG_NUMBER +internal var templateVar: Int = NUMBER \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixProject.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixProject.kt new file mode 100644 index 0000000000..3891f14aed --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixProject.kt @@ -0,0 +1,57 @@ +package net.corda.gradle.jarfilter + +import org.assertj.core.api.Assertions.* +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome.* +import org.junit.Assert.* +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import java.io.FileNotFoundException +import java.nio.file.Path + +@Suppress("UNUSED") +class MetaFixProject(private val projectDir: TemporaryFolder, private val name: String) : TestRule { + private var _sourceJar: Path? = null + val sourceJar: Path get() = _sourceJar ?: throw FileNotFoundException("Input not found") + + private var _metafixedJar: Path? = null + val metafixedJar: Path get() = _metafixedJar ?: throw FileNotFoundException("Output not found") + + private var _output: String = "" + val output: String get() = _output + + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + projectDir.installResources( + "$name/build.gradle", + "repositories.gradle", + "gradle.properties", + "settings.gradle" + ) + + val result = GradleRunner.create() + .withProjectDir(projectDir.root) + .withArguments(getGradleArgsForTasks("metafix")) + .withPluginClasspath() + .build() + _output = result.output + println(output) + + val metafix = result.task(":metafix") + ?: throw AssertionError("No outcome for metafix task") + assertEquals(SUCCESS, metafix.outcome) + + _sourceJar = projectDir.pathOf("build", "libs", "$name.jar") + assertThat(sourceJar).isRegularFile() + + _metafixedJar = projectDir.pathOf("build", "metafixer-libs", "$name-metafixed.jar") + assertThat(metafixedJar).isRegularFile() + + base.evaluate() + } + } + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixSealedClassTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixSealedClassTest.kt new file mode 100644 index 0000000000..53ac03edb6 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixSealedClassTest.kt @@ -0,0 +1,37 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.asm.* +import org.assertj.core.api.Assertions.* +import org.gradle.api.logging.Logger +import org.junit.Test +import kotlin.reflect.jvm.jvmName + +class MetaFixSealedClassTest { + companion object { + private val logger: Logger = StdOutLogging(MetaFixSealedClassTest::class) + private val UNWANTED_CLASS: String = "${MetaSealedClass::class.jvmName}\$Unwanted" + private val WANTED_CLASS: String = MetaSealedClass.Wanted::class.jvmName + } + + @Test + fun testSealedSubclassRemovedFromMetadata() { + val bytecode = recodeMetadataFor() + val sourceClass = bytecode.toClass() + assertThat(sourceClass.classMetadata.sealedSubclasses).containsExactlyInAnyOrder(UNWANTED_CLASS, WANTED_CLASS) + + // Rewrite the metadata according to the contents of the bytecode. + val fixedClass = bytecode.fixMetadata(logger, pathsOf(MetaSealedClass::class, MetaSealedClass.Wanted::class)) + .toClass() + assertThat(fixedClass.classMetadata.sealedSubclasses).containsExactly(WANTED_CLASS) + } + + @Suppress("UNUSED") + sealed class MetadataTemplate { + class Wanted : MetadataTemplate() + class Unwanted : MetadataTemplate() + } +} + +sealed class MetaSealedClass { + class Wanted : MetaSealedClass() +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixTimestampTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixTimestampTest.kt new file mode 100644 index 0000000000..f96b94558c --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixTimestampTest.kt @@ -0,0 +1,108 @@ +package net.corda.gradle.jarfilter + +import org.assertj.core.api.Assertions.* +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome.* +import org.junit.Assert.* +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import org.junit.runners.model.Statement +import java.nio.file.Path +import java.nio.file.attribute.FileTime +import java.util.* +import java.util.Calendar.FEBRUARY +import java.util.zip.ZipEntry +import java.util.zip.ZipEntry.* +import java.util.zip.ZipFile + +class MetaFixTimestampTest { + companion object { + private val testProjectDir = TemporaryFolder() + private val sourceJar = DummyJar(testProjectDir, MetaFixTimestampTest::class.java, "timestamps") + + private val CONSTANT_TIME: FileTime = FileTime.fromMillis( + GregorianCalendar(1980, FEBRUARY, 1).apply { + timeZone = TimeZone.getTimeZone("UTC") + }.timeInMillis + ) + + private lateinit var metafixedJar: Path + private lateinit var output: String + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(sourceJar) + .around(createTestProject()) + + private fun createTestProject() = TestRule { base, _ -> + object : Statement() { + override fun evaluate() { + testProjectDir.installResource("gradle.properties") + testProjectDir.newFile("build.gradle").writeText(""" +plugins { + id 'net.corda.plugins.jar-filter' +} + +import net.corda.gradle.jarfilter.MetaFixerTask +task metafix(type: MetaFixerTask) { + jars file("${sourceJar.path.toUri()}") + preserveTimestamps = false +} +""") + val result = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments(getGradleArgsForTasks("metafix")) + .withPluginClasspath() + .build() + output = result.output + println(output) + + val metafix = result.task(":metafix") + ?: throw AssertionError("No outcome for metafix task") + assertEquals(SUCCESS, metafix.outcome) + + metafixedJar = testProjectDir.pathOf("build", "metafixer-libs", "timestamps-metafixed.jar") + assertThat(metafixedJar).isRegularFile() + + base.evaluate() + } + } + } + + private val ZipEntry.methodName: String get() = if (method == STORED) "Stored" else "Deflated" + } + + @Test + fun fileTimestampsAreRemoved() { + var directoryCount = 0 + var classCount = 0 + var otherCount = 0 + + ZipFile(metafixedJar.toFile()).use { jar -> + for (entry in jar.entries()) { + println("Entry: ${entry.name}") + println("- ${entry.methodName} (${entry.size} size / ${entry.compressedSize} compressed) bytes") + assertThat(entry.lastModifiedTime).isEqualTo(CONSTANT_TIME) + assertThat(entry.lastAccessTime).isNull() + assertThat(entry.creationTime).isNull() + + if (entry.isDirectory) { + ++directoryCount + } else if (entry.name.endsWith(".class")) { + ++classCount + } else { + ++otherCount + } + } + } + + assertThat(directoryCount).isGreaterThan(0) + assertThat(classCount).isGreaterThan(0) + assertThat(otherCount).isGreaterThan(0) + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixValPropertyTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixValPropertyTest.kt new file mode 100644 index 0000000000..cef1cb2a77 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixValPropertyTest.kt @@ -0,0 +1,49 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.unwanted.* +import net.corda.gradle.jarfilter.asm.* +import net.corda.gradle.jarfilter.matcher.* +import org.gradle.api.logging.Logger +import org.hamcrest.core.IsCollectionContaining.hasItem +import org.hamcrest.core.IsNot.not +import org.junit.Assert.* +import org.junit.Test +import kotlin.jvm.kotlin +import kotlin.reflect.full.declaredMemberProperties + +class MetaFixValPropertyTest { + companion object { + private val logger: Logger = StdOutLogging(MetaFixValPropertyTest::class) + private val unwantedVal = isProperty("unwantedVal", String::class) + private val intVal = isProperty("intVal", Int::class) + } + + @Test + fun testPropertyRemovedFromMetadata() { + val bytecode = recodeMetadataFor() + val sourceClass = bytecode.toClass() + + // Check that the unwanted property has been successfully + // added to the metadata, and that the class is valid. + val sourceObj = sourceClass.newInstance() + assertEquals(NUMBER, sourceObj.intVal) + assertThat("unwantedVal not found", sourceClass.kotlin.declaredMemberProperties, hasItem(unwantedVal)) + assertThat("intVal not found", sourceClass.kotlin.declaredMemberProperties, hasItem(intVal)) + + // Rewrite the metadata according to the contents of the bytecode. + val fixedClass = bytecode.fixMetadata(logger, pathsOf(WithValProperty::class)).toClass() + val fixedObj = fixedClass.newInstance() + assertEquals(NUMBER, fixedObj.intVal) + assertThat("unwantedVal still exists", fixedClass.kotlin.declaredMemberProperties, not(hasItem(unwantedVal))) + assertThat("intVal not found", fixedClass.kotlin.declaredMemberProperties, hasItem(intVal)) + } + + class MetadataTemplate : HasIntVal { + override val intVal: Int = 0 + @Suppress("UNUSED") val unwantedVal: String = "UNWANTED" + } +} + +class WithValProperty : HasIntVal { + override val intVal: Int = NUMBER +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixVarPropertyTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixVarPropertyTest.kt new file mode 100644 index 0000000000..9d904f610a --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixVarPropertyTest.kt @@ -0,0 +1,49 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.unwanted.* +import net.corda.gradle.jarfilter.asm.* +import net.corda.gradle.jarfilter.matcher.* +import org.gradle.api.logging.Logger +import org.hamcrest.core.IsCollectionContaining.hasItem +import org.hamcrest.core.IsNot.not +import org.junit.Assert.* +import org.junit.Test +import kotlin.jvm.kotlin +import kotlin.reflect.full.declaredMemberProperties + +class MetaFixVarPropertyTest { + companion object { + private val logger: Logger = StdOutLogging(MetaFixVarPropertyTest::class) + private val unwantedVar = isProperty("unwantedVar", String::class) + private val intVar = isProperty("intVar", Int::class) + } + + @Test + fun testPropertyRemovedFromMetadata() { + val bytecode = recodeMetadataFor() + val sourceClass = bytecode.toClass() + + // Check that the unwanted property has been successfully + // added to the metadata, and that the class is valid. + val sourceObj = sourceClass.newInstance() + assertEquals(NUMBER, sourceObj.intVar) + assertThat("unwantedVar not found", sourceClass.kotlin.declaredMemberProperties, hasItem(unwantedVar)) + assertThat("intVar not found", sourceClass.kotlin.declaredMemberProperties, hasItem(intVar)) + + // Rewrite the metadata according to the contents of the bytecode. + val fixedClass = bytecode.fixMetadata(logger, pathsOf(WithVarProperty::class)).toClass() + val fixedObj = fixedClass.newInstance() + assertEquals(NUMBER, fixedObj.intVar) + assertThat("unwantedVar still exists", fixedClass.kotlin.declaredMemberProperties, not(hasItem(unwantedVar))) + assertThat("intVar not found", fixedClass.kotlin.declaredMemberProperties, hasItem(intVar)) + } + + class MetadataTemplate : HasIntVar { + override var intVar: Int = 0 + @Suppress("UNUSED") var unwantedVar: String = "UNWANTED" + } +} + +class WithVarProperty : HasIntVar { + override var intVar: Int = NUMBER +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MethodElementTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MethodElementTest.kt new file mode 100644 index 0000000000..bee8d58568 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MethodElementTest.kt @@ -0,0 +1,88 @@ +package net.corda.gradle.jarfilter + +import org.junit.Assert.* +import org.junit.Test +import org.objectweb.asm.Opcodes.* + +class MethodElementTest { + private companion object { + private const val DESCRIPTOR = "()Ljava.lang.String;" + } + + @Test + fun testMethodsMatchByNameAndDescriptor() { + val elt = MethodElement( + name = "getThing", + descriptor = DESCRIPTOR, + access = ACC_PUBLIC or ACC_ABSTRACT or ACC_FINAL + ) + assertEquals(MethodElement(name="getThing", descriptor=DESCRIPTOR), elt) + assertNotEquals(MethodElement(name="getOther", descriptor=DESCRIPTOR), elt) + assertNotEquals(MethodElement(name="getThing", descriptor="()J"), elt) + } + + @Test + fun testBasicMethodVisibleName() { + val elt = MethodElement( + name = "getThing", + descriptor = DESCRIPTOR, + access = ACC_PUBLIC + ) + assertEquals("getThing", elt.visibleName) + } + + @Test + fun testMethodVisibleNameWithSuffix() { + val elt = MethodElement( + name = "getThing\$extra", + descriptor = DESCRIPTOR, + access = ACC_PUBLIC + ) + assertEquals("getThing", elt.visibleName) + } + + @Test + fun testSyntheticMethodSuffix() { + val elt = MethodElement( + name = "getThing\$extra", + descriptor = DESCRIPTOR, + access = ACC_PUBLIC or ACC_SYNTHETIC + ) + assertTrue(elt.isKotlinSynthetic("extra")) + assertFalse(elt.isKotlinSynthetic("something")) + assertTrue(elt.isKotlinSynthetic("extra", "something")) + } + + @Test + fun testPublicMethodSuffix() { + val elt = MethodElement( + name = "getThing\$extra", + descriptor = DESCRIPTOR, + access = ACC_PUBLIC + ) + assertFalse(elt.isKotlinSynthetic("extra")) + } + + @Test + fun testMethodDoesNotExpire() { + val elt = MethodElement( + name = "getThing\$extra", + descriptor = DESCRIPTOR, + access = ACC_PUBLIC + ) + assertFalse(elt.isExpired) + assertFalse(elt.isExpired) + assertFalse(elt.isExpired) + } + + @Test + fun testArtificialMethodDoesExpire() { + val elt = MethodElement( + name = "getThing\$extra", + descriptor = DESCRIPTOR + ) + assertFalse(elt.isExpired) + assertTrue(elt.isExpired) + assertTrue(elt.isExpired) + } +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/RemoveAnnotationsTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/RemoveAnnotationsTest.kt new file mode 100644 index 0000000000..ba074903c8 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/RemoveAnnotationsTest.kt @@ -0,0 +1,176 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.unwanted.HasUnwantedFun +import net.corda.gradle.unwanted.HasUnwantedVal +import net.corda.gradle.unwanted.HasUnwantedVar +import org.junit.Assert.* +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule + +class RemoveAnnotationsTest { + companion object { + private const val ANNOTATED_CLASS = "net.corda.gradle.HasUnwantedAnnotations" + private const val REMOVE_ME_CLASS = "net.corda.gradle.jarfilter.RemoveMe" + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "remove-annotations") + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + } + + @Test + fun deleteFromClass() { + classLoaderFor(testProject.sourceJar).use { cl -> + val removeMe = cl.load(REMOVE_ME_CLASS) + cl.load(ANNOTATED_CLASS).apply { + assertNotNull(getAnnotation(removeMe)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + val removeMe = cl.load(REMOVE_ME_CLASS) + cl.load(ANNOTATED_CLASS).apply { + assertNull(getAnnotation(removeMe)) + } + } + } + + @Test + fun deleteFromDefaultConstructor() { + classLoaderFor(testProject.sourceJar).use { cl -> + val removeMe = cl.load(REMOVE_ME_CLASS) + cl.load(ANNOTATED_CLASS).apply { + getDeclaredConstructor().also { con -> + assertNotNull(con.getAnnotation(removeMe)) + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + val removeMe = cl.load(REMOVE_ME_CLASS) + cl.load(ANNOTATED_CLASS).apply { + getDeclaredConstructor().also { con -> + assertNull(con.getAnnotation(removeMe)) + } + } + } + } + + @Test + fun deleteFromPrimaryConstructor() { + classLoaderFor(testProject.sourceJar).use { cl -> + val removeMe = cl.load(REMOVE_ME_CLASS) + cl.load(ANNOTATED_CLASS).apply { + getDeclaredConstructor(Long::class.java, String::class.java).also { con -> + assertNotNull(con.getAnnotation(removeMe)) + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + val removeMe = cl.load(REMOVE_ME_CLASS) + cl.load(ANNOTATED_CLASS).apply { + getDeclaredConstructor(Long::class.java, String::class.java).also { con -> + assertNull(con.getAnnotation(removeMe)) + } + } + } + } + + @Test + fun deleteFromField() { + classLoaderFor(testProject.sourceJar).use { cl -> + val removeMe = cl.load(REMOVE_ME_CLASS) + cl.load(ANNOTATED_CLASS).apply { + getField("longField").also { field -> + assertNotNull(field.getAnnotation(removeMe)) + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + val removeMe = cl.load(REMOVE_ME_CLASS) + cl.load(ANNOTATED_CLASS).apply { + getField("longField").also { field -> + assertNull(field.getAnnotation(removeMe)) + } + } + } + } + + @Test + fun deleteFromMethod() { + classLoaderFor(testProject.sourceJar).use { cl -> + val removeMe = cl.load(REMOVE_ME_CLASS) + cl.load(ANNOTATED_CLASS).apply { + getMethod("unwantedFun", String::class.java).also { method -> + assertNotNull(method.getAnnotation(removeMe)) + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + val removeMe = cl.load(REMOVE_ME_CLASS) + cl.load(ANNOTATED_CLASS).apply { + getMethod("unwantedFun", String::class.java).also { method -> + assertNull(method.getAnnotation(removeMe)) + } + } + } + } + + @Test + fun deleteFromValProperty() { + classLoaderFor(testProject.sourceJar).use { cl -> + val removeMe = cl.load(REMOVE_ME_CLASS) + cl.load(ANNOTATED_CLASS).apply { + getMethod("getUnwantedVal").also { method -> + assertNotNull(method.getAnnotation(removeMe)) + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + val removeMe = cl.load(REMOVE_ME_CLASS) + cl.load(ANNOTATED_CLASS).apply { + getMethod("getUnwantedVal").also { method -> + assertNull(method.getAnnotation(removeMe)) + } + } + } + } + + @Test + fun deleteFromVarProperty() { + classLoaderFor(testProject.sourceJar).use { cl -> + val removeMe = cl.load(REMOVE_ME_CLASS) + cl.load(ANNOTATED_CLASS).apply { + getMethod("getUnwantedVar").also { method -> + assertNotNull(method.getAnnotation(removeMe)) + } + getMethod("setUnwantedVar", String::class.java).also { method -> + assertNotNull(method.getAnnotation(removeMe)) + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + val removeMe = cl.load(REMOVE_ME_CLASS) + cl.load(ANNOTATED_CLASS).apply { + getMethod("getUnwantedVar").also { method -> + assertNull(method.getAnnotation(removeMe)) + } + getMethod("setUnwantedVar", String::class.java).also { method -> + assertNull(method.getAnnotation(removeMe)) + } + } + } + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StaticFieldRemovalTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StaticFieldRemovalTest.kt new file mode 100644 index 0000000000..49df4a2eb7 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StaticFieldRemovalTest.kt @@ -0,0 +1,102 @@ +@file:JvmName("StaticFields") +@file:Suppress("UNUSED") +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.annotations.Deletable +import net.corda.gradle.jarfilter.asm.bytecode +import net.corda.gradle.jarfilter.asm.toClass +import org.gradle.api.logging.Logger +import org.junit.Assert.* +import org.junit.BeforeClass +import org.junit.Test +import org.objectweb.asm.ClassWriter.COMPUTE_MAXS +import kotlin.reflect.jvm.jvmName +import kotlin.test.assertFailsWith + +/** + * Static properties are all initialised in the same block. + * Show that deleting some field references doesn't break the other + * properties' initialisation code. + */ +class StaticFieldRemovalTest { + companion object { + private val logger: Logger = StdOutLogging(StaticFieldRemovalTest::class) + private const val FIELD_CLASS = "net.corda.gradle.jarfilter.StaticFields" + + private lateinit var sourceClass: Class + private lateinit var targetClass: Class + + private fun transform(type: Class, asType: Class): Class { + val bytecode = type.bytecode.execute({ writer -> + ClassTransformer( + visitor = writer, + logger = logger, + removeAnnotations = emptySet(), + deleteAnnotations = setOf(Deletable::class.jvmName.descriptor), + stubAnnotations = emptySet(), + unwantedClasses = mutableSetOf() + ) + }, COMPUTE_MAXS) + return bytecode.toClass(type, asType) + } + + @JvmStatic + @BeforeClass + fun setup() { + sourceClass = Class.forName(FIELD_CLASS) + targetClass = transform(sourceClass, Any::class.java) + } + } + + @Test + fun deleteStaticString() { + assertEquals("1", sourceClass.getDeclaredMethod("getStaticString").invoke(null)) + assertFailsWith { targetClass.getDeclaredMethod("getStaticString") } + } + + @Test + fun deleteStaticLong() { + assertEquals(2L, sourceClass.getDeclaredMethod("getStaticLong").invoke(null)) + assertFailsWith { targetClass.getDeclaredMethod("getStaticLong") } + } + + @Test + fun deleteStaticInt() { + assertEquals(3, sourceClass.getDeclaredMethod("getStaticInt").invoke(null)) + assertFailsWith { targetClass.getDeclaredMethod("getStaticInt") } + } + + @Test + fun deleteStaticShort() { + assertEquals(4.toShort(), sourceClass.getDeclaredMethod("getStaticShort").invoke(null)) + assertFailsWith { targetClass.getDeclaredMethod("getStaticShort") } + } + + @Test + fun deleteStaticByte() { + assertEquals(5.toByte(), sourceClass.getDeclaredMethod("getStaticByte").invoke(null)) + assertFailsWith { targetClass.getDeclaredMethod("getStaticByte") } + } + + @Test + fun deleteStaticChar() { + assertEquals(6.toChar(), sourceClass.getDeclaredMethod("getStaticChar").invoke(null)) + assertFailsWith { targetClass.getDeclaredMethod("getStaticChar") } + } + + @Test + fun checkSeedHasBeenIncremented() { + assertEquals(6, sourceClass.getDeclaredMethod("getStaticSeed").invoke(null)) + assertEquals(6, targetClass.getDeclaredMethod("getStaticSeed").invoke(null)) + } +} + +private var seed: Int = 0 +val staticSeed get() = seed + +@Deletable val staticString: String = (++seed).toString() +@Deletable val staticLong: Long = (++seed).toLong() +@Deletable val staticInt: Int = ++seed +@Deletable val staticShort: Short = (++seed).toShort() +@Deletable val staticByte: Byte = (++seed).toByte() +@Deletable val staticChar: Char = (++seed).toChar() diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StdOutLogging.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StdOutLogging.kt new file mode 100644 index 0000000000..7cd785cf8f --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StdOutLogging.kt @@ -0,0 +1,262 @@ +package net.corda.gradle.jarfilter + +import org.gradle.api.logging.LogLevel.* +import org.gradle.api.logging.LogLevel +import org.gradle.api.logging.Logger +import org.slf4j.Marker +import org.slf4j.helpers.MessageFormatter +import kotlin.reflect.KClass + +class StdOutLogging(private val name: String, private val threshold: LogLevel = INFO) : Logger { + constructor(clazz: KClass<*>) : this(clazz.java.simpleName) + + override fun getName(): String = name + + override fun isErrorEnabled(): Boolean = isEnabled(ERROR) + override fun isErrorEnabled(marker: Marker): Boolean = isEnabled(ERROR) + + override fun isWarnEnabled(): Boolean = isEnabled(WARN) + override fun isWarnEnabled(marker: Marker): Boolean = isEnabled(WARN) + + override fun isInfoEnabled(): Boolean = isEnabled(INFO) + override fun isInfoEnabled(marker: Marker): Boolean = isEnabled(INFO) + + override fun isDebugEnabled(): Boolean = isEnabled(DEBUG) + override fun isDebugEnabled(marker: Marker): Boolean = isEnabled(DEBUG) + + override fun isTraceEnabled(): Boolean = isEnabled(DEBUG) + override fun isTraceEnabled(marker: Marker): Boolean = isEnabled(DEBUG) + + override fun isQuietEnabled(): Boolean = isEnabled(QUIET) + + override fun isLifecycleEnabled(): Boolean = isEnabled(LIFECYCLE) + + override fun isEnabled(level: LogLevel): Boolean = threshold <= level + + override fun warn(msg: String) = log(WARN, msg) + override fun warn(msg: String, obj: Any?) = log(WARN, msg, obj) + override fun warn(msg: String, vararg objects: Any?) = log(WARN, msg, *objects) + override fun warn(msg: String, obj1: Any?, obj2: Any?) = log(WARN, msg, obj1, obj2) + override fun warn(msg: String, ex: Throwable) = log(WARN, msg, ex) + + override fun warn(marker: Marker, msg: String) { + if (isWarnEnabled(marker)) { + print(WARN, msg) + } + } + + override fun warn(marker: Marker, msg: String, obj: Any?) { + if (isWarnEnabled(marker)) { + print(WARN, msg, obj) + } + } + + override fun warn(marker: Marker, msg: String, obj1: Any?, obj2: Any?) { + if (isWarnEnabled(marker)) { + print(WARN, msg, obj1, obj2) + } + } + + override fun warn(marker: Marker, msg: String, vararg objects: Any?) { + if (isWarnEnabled(marker)) { + printAny(WARN, msg, *objects) + } + } + + override fun warn(marker: Marker, msg: String, ex: Throwable) { + if (isWarnEnabled(marker)) { + print(WARN, msg, ex) + } + } + + override fun info(message: String, vararg objects: Any?) = log(INFO, message, *objects) + override fun info(message: String) = log(INFO, message) + override fun info(message: String, obj: Any?) = log(INFO, message, obj) + override fun info(message: String, obj1: Any?, obj2: Any?) = log(INFO, message, obj1, obj2) + override fun info(message: String, ex: Throwable) = log(INFO, message, ex) + + override fun info(marker: Marker, msg: String) { + if (isInfoEnabled(marker)) { + print(INFO, msg) + } + } + + override fun info(marker: Marker, msg: String, obj: Any?) { + if (isInfoEnabled(marker)) { + print(INFO, msg, obj) + } + } + + override fun info(marker: Marker, msg: String, obj1: Any?, obj2: Any?) { + if (isInfoEnabled(marker)) { + print(INFO, msg, obj1, obj2) + } + } + + override fun info(marker: Marker, msg: String, vararg objects: Any?) { + if (isInfoEnabled(marker)) { + printAny(INFO, msg, *objects) + } + } + + override fun info(marker: Marker, msg: String, ex: Throwable) { + if (isInfoEnabled(marker)) { + print(INFO, msg, ex) + } + } + + override fun error(message: String) = log(ERROR, message) + override fun error(message: String, obj: Any?) = log(ERROR, message, obj) + override fun error(message: String, obj1: Any?, obj2: Any?) = log(ERROR, message, obj1, obj2) + override fun error(message: String, vararg objects: Any?) = log(ERROR, message, *objects) + override fun error(message: String, ex: Throwable) = log(ERROR, message, ex) + + override fun error(marker: Marker, msg: String) { + if (isErrorEnabled(marker)) { + print(ERROR, msg) + } + } + + override fun error(marker: Marker, msg: String, obj: Any?) { + if (isErrorEnabled(marker)) { + print(ERROR, msg, obj) + } + } + + override fun error(marker: Marker, msg: String, obj1: Any?, obj2: Any?) { + if (isErrorEnabled(marker)) { + print(ERROR, msg, obj1, obj2) + } + } + + override fun error(marker: Marker, msg: String, vararg objects: Any?) { + if (isErrorEnabled(marker)) { + printAny(ERROR, msg, *objects) + } + } + + override fun error(marker: Marker, msg: String, ex: Throwable) { + if (isErrorEnabled(marker)) { + print(ERROR, msg, ex) + } + } + + override fun log(level: LogLevel, message: String) { + if (isEnabled(level)) { + print(level, message) + } + } + + override fun log(level: LogLevel, message: String, vararg objects: Any?) { + if (isEnabled(level)) { + printAny(level, message, *objects) + } + } + + override fun log(level: LogLevel, message: String, ex: Throwable) { + if (isEnabled(level)) { + print(level, message, ex) + } + } + + override fun debug(message: String, vararg objects: Any?) = log(DEBUG, message, *objects) + override fun debug(message: String) = log(DEBUG, message) + override fun debug(message: String, obj: Any?) = log(DEBUG, message, obj) + override fun debug(message: String, obj1: Any?, obj2: Any?) = log(DEBUG, message, obj1, obj2) + override fun debug(message: String, ex: Throwable) = log(DEBUG, message, ex) + + override fun debug(marker: Marker, msg: String) { + if (isDebugEnabled(marker)) { + print(DEBUG, msg) + } + } + + override fun debug(marker: Marker, msg: String, obj: Any?) { + if (isDebugEnabled(marker)) { + print(DEBUG, msg, obj) + } + } + + override fun debug(marker: Marker, msg: String, obj1: Any?, obj2: Any?) { + if (isDebugEnabled(marker)) { + print(DEBUG, msg, obj1, obj2) + } + } + + override fun debug(marker: Marker, msg: String, vararg objects: Any?) { + if (isDebugEnabled(marker)) { + printAny(DEBUG, msg, *objects) + } + } + + override fun debug(marker: Marker, msg: String, ex: Throwable) { + if (isDebugEnabled(marker)) { + print(DEBUG, msg, ex) + } + } + + override fun lifecycle(message: String) = log(LIFECYCLE, message) + override fun lifecycle(message: String, vararg objects: Any?) = log(LIFECYCLE, message, *objects) + override fun lifecycle(message: String, ex: Throwable) = log(LIFECYCLE, message, ex) + + override fun quiet(message: String) = log(QUIET, message) + override fun quiet(message: String, vararg objects: Any?) = log(QUIET, message, *objects) + override fun quiet(message: String, ex: Throwable) = log(QUIET, message, ex) + + override fun trace(message: String) = debug(message) + override fun trace(message: String, obj: Any?) = debug(message, obj) + override fun trace(message: String, obj1: Any?, obj2: Any?) = debug(message, obj1, obj2) + override fun trace(message: String, vararg objects: Any?) = debug(message, *objects) + override fun trace(message: String, ex: Throwable) = debug(message, ex) + + override fun trace(marker: Marker, msg: String) { + if (isTraceEnabled(marker)) { + print(DEBUG, msg) + } + } + + override fun trace(marker: Marker, msg: String, obj: Any?) { + if (isTraceEnabled(marker)) { + print(DEBUG, msg, obj) + } + } + + override fun trace(marker: Marker, msg: String, obj1: Any?, obj2: Any?) { + if (isTraceEnabled(marker)) { + print(DEBUG, msg, obj1, obj2) + } + } + + override fun trace(marker: Marker, msg: String, vararg objects: Any?) { + if (isTraceEnabled(marker)) { + printAny(DEBUG, msg, *objects) + } + } + + override fun trace(marker: Marker, msg: String, ex: Throwable) { + if (isTraceEnabled) { + print(DEBUG, msg, ex) + } + } + + private fun print(level: LogLevel, message: String) { + println("$name - $level: $message") + } + + private fun print(level: LogLevel, message: String, ex: Throwable) { + print(level, message) + ex.printStackTrace(System.out) + } + + private fun print(level: LogLevel, message: String, obj: Any?) { + print(level, MessageFormatter.format(message, obj).message) + } + + private fun print(level: LogLevel, message: String, obj1: Any?, obj2: Any?) { + print(level, MessageFormatter.format(message, obj1, obj2).message) + } + + private fun printAny(level: LogLevel, message: String, vararg objects: Any?) { + print(level, MessageFormatter.arrayFormat(message, objects).message) + } +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StubConstructorTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StubConstructorTest.kt new file mode 100644 index 0000000000..56352be355 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StubConstructorTest.kt @@ -0,0 +1,160 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.unwanted.HasAll +import net.corda.gradle.unwanted.HasInt +import net.corda.gradle.unwanted.HasLong +import net.corda.gradle.unwanted.HasString +import org.assertj.core.api.Assertions.* +import org.junit.Assert.* +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import java.lang.reflect.InvocationTargetException +import kotlin.test.assertFailsWith + +class StubConstructorTest { + companion object { + private const val STRING_PRIMARY_CONSTRUCTOR_CLASS = "net.corda.gradle.PrimaryStringConstructorToStub" + private const val LONG_PRIMARY_CONSTRUCTOR_CLASS = "net.corda.gradle.PrimaryLongConstructorToStub" + private const val INT_PRIMARY_CONSTRUCTOR_CLASS = "net.corda.gradle.PrimaryIntConstructorToStub" + private const val SECONDARY_CONSTRUCTOR_CLASS = "net.corda.gradle.HasConstructorToStub" + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "stub-constructor") + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + } + + @Test + fun stubConstructorWithLongParameter() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(SECONDARY_CONSTRUCTOR_CLASS).apply { + getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER).also { obj -> + assertEquals(BIG_NUMBER, obj.longData()) + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(SECONDARY_CONSTRUCTOR_CLASS).apply { + getDeclaredConstructor(Long::class.java).also { + assertFailsWith { it.newInstance(BIG_NUMBER) }.targetException.also { ex -> + assertThat(ex) + .isInstanceOf(UnsupportedOperationException::class.java) + .hasMessage("Method has been deleted") + } + } + } + } + } + + @Test + fun stubConstructorWithStringParameter() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(SECONDARY_CONSTRUCTOR_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj -> + assertEquals(MESSAGE, obj.stringData()) + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(SECONDARY_CONSTRUCTOR_CLASS).apply { + getDeclaredConstructor(String::class.java).also { + assertFailsWith { it.newInstance(MESSAGE) }.targetException.also { ex -> + assertThat(ex) + .isInstanceOf(UnsupportedOperationException::class.java) + .hasMessage("Method has been deleted") + } + } + } + } + } + + @Test + fun showUnannotatedConstructorIsUnaffected() { + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(SECONDARY_CONSTRUCTOR_CLASS).apply { + getDeclaredConstructor(Int::class.java).newInstance(NUMBER).also { obj -> + assertEquals(NUMBER, obj.intData()) + assertEquals(NUMBER.toLong(), obj.longData()) + assertEquals("", obj.stringData()) + } + } + } + } + + @Test + fun stubPrimaryConstructorWithStringParameter() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(STRING_PRIMARY_CONSTRUCTOR_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj -> + assertEquals(MESSAGE, obj.stringData()) + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(STRING_PRIMARY_CONSTRUCTOR_CLASS).apply { + getDeclaredConstructor(String::class.java).also { + assertFailsWith { it.newInstance(MESSAGE) }.targetException.also { ex -> + assertThat(ex) + .isInstanceOf(UnsupportedOperationException::class.java) + .hasMessage("Method has been deleted") + } + } + } + } + } + + @Test + fun stubPrimaryConstructorWithLongParameter() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(LONG_PRIMARY_CONSTRUCTOR_CLASS).apply { + getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER).also { obj -> + assertEquals(BIG_NUMBER, obj.longData()) + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(LONG_PRIMARY_CONSTRUCTOR_CLASS).apply { + getDeclaredConstructor(Long::class.java).also { + assertFailsWith { it.newInstance(BIG_NUMBER) }.targetException.also { ex -> + assertThat(ex) + .isInstanceOf(UnsupportedOperationException::class.java) + .hasMessage("Method has been deleted") + } + } + } + } + } + + @Test + fun stubPrimaryConstructorWithIntParameter() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(INT_PRIMARY_CONSTRUCTOR_CLASS).apply { + getDeclaredConstructor(Int::class.java).newInstance(NUMBER).also { obj -> + assertEquals(NUMBER, obj.intData()) + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(INT_PRIMARY_CONSTRUCTOR_CLASS).apply { + getDeclaredConstructor(Int::class.java).apply { + val error = assertFailsWith { newInstance(NUMBER) }.targetException + assertThat(error) + .isInstanceOf(UnsupportedOperationException::class.java) + .hasMessage("Method has been deleted") + } + } + } + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StubFunctionOutTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StubFunctionOutTest.kt new file mode 100644 index 0000000000..333fa2ce56 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StubFunctionOutTest.kt @@ -0,0 +1,74 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.unwanted.HasUnwantedFun +import org.assertj.core.api.Assertions.* +import org.junit.Assert.* +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import javax.annotation.Resource +import kotlin.test.assertFailsWith + +class StubFunctionOutTest { + companion object { + private const val FUNCTION_CLASS = "net.corda.gradle.HasFunctionToStub" + private const val STUB_ME_OUT_ANNOTATION = "net.corda.gradle.jarfilter.StubMeOut" + private const val PARAMETER_ANNOTATION = "net.corda.gradle.Parameter" + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "stub-function") + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + } + + @Test + fun stubFunction() { + classLoaderFor(testProject.sourceJar).use { cl -> + val stubMeOut = cl.load(STUB_ME_OUT_ANNOTATION) + val parameter = cl.load(PARAMETER_ANNOTATION) + + cl.load(FUNCTION_CLASS).apply { + newInstance().also { obj -> + assertEquals(MESSAGE, obj.unwantedFun(MESSAGE)) + } + getMethod("unwantedFun", String::class.java).also { method -> + assertTrue("StubMeOut annotation missing", method.isAnnotationPresent (stubMeOut)) + assertTrue("Resource annotation missing", method.isAnnotationPresent(Resource::class.java)) + method.parameterAnnotations.also { paramAnns -> + assertEquals(1, paramAnns.size) + assertThat(paramAnns[0]) + .hasOnlyOneElementSatisfying { a -> a.javaClass.isInstance(parameter) } + } + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + val stubMeOut = cl.load(STUB_ME_OUT_ANNOTATION) + val parameter = cl.load(PARAMETER_ANNOTATION) + + cl.load(FUNCTION_CLASS).apply { + newInstance().also { obj -> + assertFailsWith { obj.unwantedFun(MESSAGE) }.also { ex -> + assertEquals("Method has been deleted", ex.message) + } + } + getMethod("unwantedFun", String::class.java).also { method -> + assertFalse("StubMeOut annotation present", method.isAnnotationPresent(stubMeOut)) + assertTrue("Resource annotation missing", method.isAnnotationPresent(Resource::class.java)) + method.parameterAnnotations.also { paramAnns -> + assertEquals(1, paramAnns.size) + assertThat(paramAnns[0]) + .hasOnlyOneElementSatisfying { a -> a.javaClass.isInstance(parameter) } + } + } + } + } + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StubStaticFunctionTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StubStaticFunctionTest.kt new file mode 100644 index 0000000000..79a7ed3b7c --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StubStaticFunctionTest.kt @@ -0,0 +1,127 @@ +package net.corda.gradle.jarfilter + +import org.assertj.core.api.Assertions.* +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import java.lang.reflect.InvocationTargetException +import kotlin.test.* + +class StubStaticFunctionTest { + companion object { + private const val FUNCTION_CLASS = "net.corda.gradle.StaticFunctionsToStub" + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "stub-static-function") + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + } + + @Test + fun stubStringFunction() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + getDeclaredMethod("unwantedStringToStub", String::class.java).also { method -> + method.invoke(null, MESSAGE).also { result -> + assertThat(result) + .isInstanceOf(String::class.java) + .isEqualTo(MESSAGE) + } + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + getDeclaredMethod("unwantedStringToStub", String::class.java).also { method -> + assertFailsWith { method.invoke(null, MESSAGE) }.targetException.also { ex -> + assertThat(ex) + .isInstanceOf(UnsupportedOperationException::class.java) + .hasMessage("Method has been deleted") + } + } + } + } + } + + @Test + fun stubLongFunction() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + getDeclaredMethod("unwantedLongToStub", Long::class.java).also { method -> + method.invoke(null, BIG_NUMBER).also { result -> + assertThat(result) + .isInstanceOf(Long::class.javaObjectType) + .isEqualTo(BIG_NUMBER) + } + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + getDeclaredMethod("unwantedLongToStub", Long::class.java).also { method -> + assertFailsWith { method.invoke(null, BIG_NUMBER) }.targetException.also { ex -> + assertThat(ex) + .isInstanceOf(UnsupportedOperationException::class.java) + .hasMessage("Method has been deleted") + } + } + } + } + } + + @Test + fun stubIntFunction() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + getDeclaredMethod("unwantedIntToStub", Int::class.java).also { method -> + method.invoke(null, NUMBER).also { result -> + assertThat(result) + .isInstanceOf(Int::class.javaObjectType) + .isEqualTo(NUMBER) + } + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + getDeclaredMethod("unwantedIntToStub", Int::class.java).also { method -> + assertFailsWith { method.invoke(null, NUMBER) }.targetException.also { ex -> + assertThat(ex) + .isInstanceOf(UnsupportedOperationException::class.java) + .hasMessage("Method has been deleted") + } + } + } + } + } + + @Test + fun stubVoidFunction() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + val staticSeed = getDeclaredMethod("getStaticSeed") + assertEquals(0, staticSeed.invoke(null)) + getDeclaredMethod("unwantedVoidToStub").invoke(null) + assertEquals(1, staticSeed.invoke(null)) + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(FUNCTION_CLASS).apply { + val staticSeed = getDeclaredMethod("getStaticSeed") + assertEquals(0, staticSeed.invoke(null)) + getDeclaredMethod("unwantedVoidToStub").invoke(null) + assertEquals(0, staticSeed.invoke(null)) + } + } + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StubValPropertyTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StubValPropertyTest.kt new file mode 100644 index 0000000000..fbc26f2211 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StubValPropertyTest.kt @@ -0,0 +1,46 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.unwanted.HasUnwantedVal +import org.junit.Assert.* +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import kotlin.test.assertFailsWith + +class StubValPropertyTest { + companion object { + private const val PROPERTY_CLASS = "net.corda.gradle.HasValPropertyForStub" + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "stub-val-property") + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + } + + @Test + fun deleteGetter() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj -> + assertEquals(MESSAGE, obj.unwantedVal) + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(PROPERTY_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj -> + assertFailsWith { obj.unwantedVal }.also { ex -> + assertEquals("Method has been deleted", ex.message) + } + } + } + } + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StubVarPropertyTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StubVarPropertyTest.kt new file mode 100644 index 0000000000..4910e83d62 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StubVarPropertyTest.kt @@ -0,0 +1,70 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.unwanted.HasUnwantedVar +import org.junit.Assert.* +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import kotlin.test.assertFailsWith + +class StubVarPropertyTest { + companion object { + private const val GETTER_CLASS = "net.corda.gradle.HasUnwantedGetForStub" + private const val SETTER_CLASS = "net.corda.gradle.HasUnwantedSetForStub" + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "stub-var-property") + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + } + + @Test + fun deleteGetter() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(GETTER_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj -> + assertEquals(MESSAGE, obj.unwantedVar) + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(GETTER_CLASS).apply { + getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj -> + assertFailsWith { obj.unwantedVar }.also { ex -> + assertEquals("Method has been deleted", ex.message) + } + } + } + } + } + + @Test + fun deleteSetter() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(SETTER_CLASS).apply { + getConstructor(String::class.java).newInstance(DEFAULT_MESSAGE).also { obj -> + assertEquals(DEFAULT_MESSAGE, obj.unwantedVar) + obj.unwantedVar = MESSAGE + assertEquals(MESSAGE, obj.unwantedVar) + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(SETTER_CLASS).apply { + getConstructor(String::class.java).newInstance(DEFAULT_MESSAGE).also { obj -> + assertEquals(DEFAULT_MESSAGE, obj.unwantedVar) + obj.unwantedVar = MESSAGE + assertEquals(DEFAULT_MESSAGE, obj.unwantedVar) + } + } + } + } +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/Utilities.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/Utilities.kt new file mode 100644 index 0000000000..62a0a36cff --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/Utilities.kt @@ -0,0 +1,82 @@ +@file:JvmName("Utilities") +package net.corda.gradle.jarfilter + +import org.junit.AssumptionViolatedException +import org.junit.rules.TemporaryFolder +import java.io.File +import java.io.IOException +import java.net.MalformedURLException +import java.net.URLClassLoader +import java.nio.file.StandardCopyOption.* +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.util.stream.Collectors.* +import java.util.zip.ZipFile +import kotlin.reflect.KClass + +const val DEFAULT_MESSAGE = "" +const val MESSAGE = "Goodbye, Cruel World!" +const val NUMBER = 111 +const val BIG_NUMBER = 9999L + +private val classLoader: ClassLoader = object {}.javaClass.classLoader + +// The AssumptionViolatedException must be caught by the JUnit test runner, +// which means that it must not be thrown when this class loads. +private val testGradleUserHomeValue: String? = System.getProperty("test.gradle.user.home") +private val testGradleUserHome: String get() = testGradleUserHomeValue + ?: throw AssumptionViolatedException("System property 'test.gradle.user.home' not set.") + +fun getGradleArgsForTasks(vararg taskNames: String): MutableList = getBasicArgsForTasks(*taskNames).apply { add("--info") } +fun getBasicArgsForTasks(vararg taskNames: String): MutableList = mutableListOf(*taskNames, "-g", testGradleUserHome) + +@Throws(IOException::class) +fun copyResourceTo(resourceName: String, target: Path) { + classLoader.getResourceAsStream(resourceName).use { source -> + Files.copy(source, target, REPLACE_EXISTING) + } +} + +@Throws(IOException::class) +fun copyResourceTo(resourceName: String, target: File) = copyResourceTo(resourceName, target.toPath()) + +@Throws(IOException::class) +fun TemporaryFolder.installResources(vararg resourceNames: String) { + resourceNames.forEach { installResource(it) } +} + +@Throws(IOException::class) +fun TemporaryFolder.installResource(resourceName: String): File = newFile(resourceName.fileName).let { file -> + copyResourceTo(resourceName, file) + file +} + +private val String.fileName: String get() = substring(1 + lastIndexOf('/')) + +val String.toPackageFormat: String get() = replace('/', '.') +fun pathsOf(vararg types: KClass<*>): Set = types.map { it.java.name.toPathFormat }.toSet() + +fun TemporaryFolder.pathOf(vararg elements: String): Path = Paths.get(root.absolutePath, *elements) + +fun arrayOfJunk(size: Int) = ByteArray(size).apply { + for (i in 0 until size) { + this[i] = (i and 0xFF).toByte() + } +} + +@Throws(MalformedURLException::class) +fun classLoaderFor(jar: Path) = URLClassLoader(arrayOf(jar.toUri().toURL()), classLoader) + +@Suppress("UNCHECKED_CAST") +@Throws(ClassNotFoundException::class) +fun ClassLoader.load(className: String) + = Class.forName(className, true, this) as Class + +fun Path.getClassNames(prefix: String): List { + val resourcePrefix = prefix.toPathFormat + return ZipFile(toFile()).stream() + .filter { it.name.startsWith(resourcePrefix) && it.name.endsWith(".class") } + .map { it.name.removeSuffix(".class").toPackageFormat } + .collect(toList()) +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/UtilsTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/UtilsTest.kt new file mode 100644 index 0000000000..b65a31130a --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/UtilsTest.kt @@ -0,0 +1,42 @@ +package net.corda.gradle.jarfilter + +import org.assertj.core.api.Assertions.assertThat +import org.gradle.api.GradleException +import org.gradle.api.InvalidUserDataException +import org.junit.Test +import java.io.IOException +import kotlin.test.assertFailsWith + +class UtilsTest { + @Test + fun testRethrowingCheckedException() { + val ex = assertFailsWith { rethrowAsUncheckedException(IOException(MESSAGE)) } + assertThat(ex) + .hasMessage(MESSAGE) + .hasCauseExactlyInstanceOf(IOException::class.java) + } + + @Test + fun testRethrowingCheckExceptionWithoutMessage() { + val ex = assertFailsWith { rethrowAsUncheckedException(IOException()) } + assertThat(ex) + .hasMessage("") + .hasCauseExactlyInstanceOf(IOException::class.java) + } + + @Test + fun testRethrowingUncheckedException() { + val ex = assertFailsWith { rethrowAsUncheckedException(IllegalArgumentException(MESSAGE)) } + assertThat(ex) + .hasMessage(MESSAGE) + .hasNoCause() + } + + @Test + fun testRethrowingGradleException() { + val ex = assertFailsWith { rethrowAsUncheckedException(InvalidUserDataException(MESSAGE)) } + assertThat(ex) + .hasMessage(MESSAGE) + .hasNoCause() + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/annotations/Deletable.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/annotations/Deletable.kt new file mode 100644 index 0000000000..3c9597c75c --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/annotations/Deletable.kt @@ -0,0 +1,8 @@ +package net.corda.gradle.jarfilter.annotations + +import kotlin.annotation.AnnotationRetention.BINARY +import kotlin.annotation.AnnotationTarget.PROPERTY + +@Retention(BINARY) +@Target(PROPERTY) +annotation class Deletable diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/asm/AsmTools.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/asm/AsmTools.kt new file mode 100644 index 0000000000..4c488d9dec --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/asm/AsmTools.kt @@ -0,0 +1,47 @@ +@file:JvmName("AsmTools") +package net.corda.gradle.jarfilter.asm + +import net.corda.gradle.jarfilter.descriptor +import net.corda.gradle.jarfilter.toPathFormat +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.ClassWriter.COMPUTE_MAXS +import java.io.ByteArrayInputStream +import java.io.InputStream + + +fun ByteArray.accept(visitor: (ClassVisitor) -> ClassVisitor): ByteArray { + return ClassWriter(COMPUTE_MAXS).let { writer -> + ClassReader(this).accept(visitor(writer), 0) + writer.toByteArray() + } +} + +private val String.resourceName: String get() = "$toPathFormat.class" +val Class<*>.resourceName get() = name.resourceName +val Class<*>.bytecode: ByteArray get() = classLoader.getResourceAsStream(resourceName).use { it.readBytes() } +val Class<*>.descriptor: String get() = name.descriptor + +/** + * Functions for converting bytecode into a "live" Java class. + */ +inline fun ByteArray.toClass(): Class = toClass(T::class.java, R::class.java) + +fun ByteArray.toClass(type: Class, asType: Class): Class + = BytecodeClassLoader(this, type.name, type.classLoader).createClass().asSubclass(asType) + +private class BytecodeClassLoader( + private val bytecode: ByteArray, + private val className: String, + parent: ClassLoader +) : ClassLoader(parent) { + internal fun createClass(): Class<*> { + return defineClass(className, bytecode, 0, bytecode.size).apply { resolveClass(this) } + } + + // Ensure that the class we create also honours Class<*>.bytecode (above). + override fun getResourceAsStream(name: String): InputStream? { + return if (name == className.resourceName) ByteArrayInputStream(bytecode) else super.getResourceAsStream(name) + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/asm/ClassMetadata.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/asm/ClassMetadata.kt new file mode 100644 index 0000000000..b4852deaa7 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/asm/ClassMetadata.kt @@ -0,0 +1,47 @@ +package net.corda.gradle.jarfilter.asm + +import net.corda.gradle.jarfilter.MetadataTransformer +import net.corda.gradle.jarfilter.toPackageFormat +import net.corda.gradle.jarfilter.mutableList +import org.gradle.api.logging.Logger +import org.jetbrains.kotlin.metadata.ProtoBuf +import org.jetbrains.kotlin.metadata.deserialization.TypeTable + +internal class ClassMetadata( + logger: Logger, + d1: List, + d2: List +) : MetadataTransformer( + logger, + emptyList(), + emptyList(), + emptyList(), + emptyList(), + emptyList(), + {}, + d1, + d2, + ProtoBuf.Class::parseFrom +) { + override val typeTable = TypeTable(message.typeTable) + override val className = nameResolver.getString(message.fqName) + override val nestedClassNames = mutableList(message.nestedClassNameList) + override val properties = mutableList(message.propertyList) + override val functions = mutableList(message.functionList) + override val constructors = mutableList(message.constructorList) + override val typeAliases = mutableList(message.typeAliasList) + override val sealedSubclassNames = mutableList(message.sealedSubclassFqNameList) + + override fun rebuild(): ProtoBuf.Class = message + + val sealedSubclasses: List = sealedSubclassNames.map { + // Transform "a/b/c/BaseName.SubclassName" -> "a.b.c.BaseName$SubclassName" + nameResolver.getString(it).replace('.', '$').toPackageFormat }.toList() + + val nestedClasses: List + + init { + val internalClassName = className.toPackageFormat + nestedClasses = nestedClassNames.map { "$internalClassName\$${nameResolver.getString(it)}" }.toList() + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/asm/FileMetadata.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/asm/FileMetadata.kt new file mode 100644 index 0000000000..f7636c810c --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/asm/FileMetadata.kt @@ -0,0 +1,33 @@ +package net.corda.gradle.jarfilter.asm + +import net.corda.gradle.jarfilter.MetadataTransformer +import net.corda.gradle.jarfilter.mutableList +import org.gradle.api.logging.Logger +import org.jetbrains.kotlin.metadata.ProtoBuf +import org.jetbrains.kotlin.metadata.deserialization.TypeTable + +internal class FileMetadata( + logger: Logger, + d1: List, + d2: List +) : MetadataTransformer( + logger, + emptyList(), + emptyList(), + emptyList(), + emptyList(), + emptyList(), + {}, + d1, + d2, + ProtoBuf.Package::parseFrom +) { + override val typeTable = TypeTable(message.typeTable) + override val properties = mutableList(message.propertyList) + override val functions = mutableList(message.functionList) + override val typeAliases = mutableList(message.typeAliasList) + + override fun rebuild(): ProtoBuf.Package = message + + val typeAliasNames: List = typeAliases.map { nameResolver.getString(it.name) }.toList() +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/asm/MetadataTools.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/asm/MetadataTools.kt new file mode 100644 index 0000000000..5526c5056d --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/asm/MetadataTools.kt @@ -0,0 +1,86 @@ +@file:JvmName("MetadataTools") +package net.corda.gradle.jarfilter.asm + +import net.corda.gradle.jarfilter.StdOutLogging +import org.jetbrains.kotlin.load.java.JvmAnnotationNames.* +import org.objectweb.asm.* +import org.objectweb.asm.Opcodes.ASM6 + +@Suppress("UNCHECKED_CAST") +private val metadataClass: Class + = object {}.javaClass.classLoader.loadClass("kotlin.Metadata") as Class + +/** + * Rewrite the bytecode for this class with the Kotlin @Metadata of another class. + */ +inline fun recodeMetadataFor(): ByteArray = T::class.java.metadataAs(X::class.java) + +fun Class.metadataAs(template: Class): ByteArray { + val metadata = template.readMetadata().let { m -> + val templateDescriptor = template.descriptor + val templatePrefix = templateDescriptor.dropLast(1) + '$' + val targetDescriptor = descriptor + val targetPrefix = targetDescriptor.dropLast(1) + '$' + Pair(m.first, m.second.map { s -> + when { + // Replace any references to the template class with the target class. + s == templateDescriptor -> targetDescriptor + s.startsWith(templatePrefix) -> targetPrefix + s.substring(templatePrefix.length) + else -> s + } + }.toList()) + } + return bytecode.accept { w -> MetadataWriter(metadata, w) } +} + +/** + * Kotlin reflection only supports classes atm, so use this to examine file metadata. + */ +internal val Class<*>.fileMetadata: FileMetadata get() { + val (d1, d2) = readMetadata() + return FileMetadata(StdOutLogging(kotlin), d1, d2) +} + +/** + * For accessing the parts of class metadata that Kotlin reflection cannot reach. + */ +internal val Class<*>.classMetadata: ClassMetadata get() { + val (d1, d2) = readMetadata() + return ClassMetadata(StdOutLogging(kotlin), d1, d2) +} + +private fun Class<*>.readMetadata(): Pair, List> { + val metadata = getAnnotation(metadataClass) + val d1 = metadataClass.getMethod(METADATA_DATA_FIELD_NAME) + val d2 = metadataClass.getMethod(METADATA_STRINGS_FIELD_NAME) + return Pair(d1.invoke(metadata).asList(), d2.invoke(metadata).asList()) +} + +@Suppress("UNCHECKED_CAST") +fun Any.asList(): List { + return (this as? Array)?.toList() ?: emptyList() +} + +private class MetadataWriter(metadata: Pair, List>, visitor: ClassVisitor) : ClassVisitor(ASM6, visitor) { + private val kotlinMetadata: MutableMap> = mutableMapOf( + METADATA_DATA_FIELD_NAME to metadata.first, + METADATA_STRINGS_FIELD_NAME to metadata.second + ) + + override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor? { + val av = super.visitAnnotation(descriptor, visible) ?: return null + return if (descriptor == METADATA_DESC) KotlinMetadataWriter(av) else av + } + + private inner class KotlinMetadataWriter(av: AnnotationVisitor) : AnnotationVisitor(api, av) { + override fun visitArray(name: String): AnnotationVisitor? { + val av = super.visitArray(name) + if (av != null) { + val data = kotlinMetadata.remove(name) ?: return av + data.forEach { av.visit(null, it) } + av.visitEnd() + } + return null + } + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/matcher/JavaMatchers.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/matcher/JavaMatchers.kt new file mode 100644 index 0000000000..25a82e6c8d --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/matcher/JavaMatchers.kt @@ -0,0 +1,79 @@ +@file:JvmName("JavaMatchers") +package net.corda.gradle.jarfilter.matcher + +import org.hamcrest.Description +import org.hamcrest.DiagnosingMatcher +import org.hamcrest.Matcher +import org.hamcrest.core.IsEqual.* +import java.lang.reflect.Method +import kotlin.reflect.KClass + +fun isMethod(name: Matcher, returnType: Matcher>, vararg parameters: Matcher>): Matcher { + return MethodMatcher(name, returnType, *parameters) +} + +fun isMethod(name: String, returnType: Class<*>, vararg parameters: Class<*>): Matcher { + return isMethod(equalTo(name), equalTo(returnType), *parameters.map(::equalTo).toTypedArray()) +} + +val KClass.javaDeclaredMethods: List get() = java.declaredMethods.toList() + +/** + * Matcher logic for a Java [Method] object. Also applicable to constructors. + */ +private class MethodMatcher( + private val name: Matcher, + private val returnType: Matcher>, + vararg parameters: Matcher> +) : DiagnosingMatcher() { + private val parameters = listOf(*parameters) + + override fun describeTo(description: Description) { + description.appendText("Method[name as ").appendDescriptionOf(name) + .appendText(", returnType as ").appendDescriptionOf(returnType) + .appendText(", parameters as '") + if (parameters.isNotEmpty()) { + val param = parameters.iterator() + description.appendValue(param.next()) + while (param.hasNext()) { + description.appendText(",").appendValue(param.next()) + } + } + description.appendText("']") + } + + override fun matches(obj: Any?, mismatch: Description): Boolean { + if (obj == null) { + mismatch.appendText("is null") + return false + } + + val method: Method = obj as? Method ?: return false + if (!name.matches(method.name)) { + mismatch.appendText("name is ").appendValue(method.name) + return false + } + method.returnType.apply { + if (!returnType.matches(this)) { + mismatch.appendText("returnType is ").appendValue(this.name) + return false + } + } + + if (method.parameterTypes.size != parameters.size) { + mismatch.appendText("number of parameters is ").appendValue(method.parameterTypes.size) + .appendText(", parameters=").appendValueList("[", ",", "]", method.parameterTypes) + return false + } + + var i = 0 + method.parameterTypes.forEach { param -> + if (!parameters[i].matches(param)) { + mismatch.appendText("parameter[").appendValue(i).appendText("] is ").appendValue(param) + return false + } + ++i + } + return true + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/matcher/KotlinMatchers.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/matcher/KotlinMatchers.kt new file mode 100644 index 0000000000..2082ddc4dc --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/matcher/KotlinMatchers.kt @@ -0,0 +1,193 @@ +@file:JvmName("KotlinMatchers") +package net.corda.gradle.jarfilter.matcher + +import org.hamcrest.Description +import org.hamcrest.DiagnosingMatcher +import org.hamcrest.Matcher +import org.hamcrest.core.IsEqual.equalTo +import kotlin.reflect.KClass +import kotlin.reflect.KFunction +import kotlin.reflect.KParameter +import kotlin.reflect.KProperty +import kotlin.reflect.full.valueParameters +import kotlin.reflect.jvm.jvmName + +fun isFunction(name: Matcher, returnType: Matcher, vararg parameters: Matcher): Matcher> { + return KFunctionMatcher(name, returnType, *parameters) +} + +fun isFunction(name: String, returnType: KClass<*>, vararg parameters: KClass<*>): Matcher> { + return isFunction(equalTo(name), matches(returnType), *parameters.map(::hasParam).toTypedArray()) +} + +fun isConstructor(returnType: Matcher, vararg parameters: Matcher): Matcher> { + return KFunctionMatcher(equalTo(""), returnType, *parameters) +} + +fun isConstructor(returnType: KClass<*>, vararg parameters: KClass<*>): Matcher> { + return isConstructor(matches(returnType), *parameters.map(::hasParam).toTypedArray()) +} + +fun isConstructor(returnType: String, vararg parameters: Matcher): Matcher> { + return isConstructor(equalTo(returnType), *parameters) +} + +fun hasParam(type: Matcher): Matcher = KParameterMatcher(type) + +fun hasParam(type: KClass<*>): Matcher = hasParam(matches(type)) + +fun isProperty(name: String, type: KClass<*>): Matcher> = isProperty(equalTo(name), matches(type)) + +fun isProperty(name: Matcher, type: Matcher): Matcher> = KPropertyMatcher(name, type) + +fun isClass(name: String): Matcher> = KClassMatcher(equalTo(name)) + +fun matches(type: KClass<*>): Matcher = equalTo(type.qualifiedName) + +/** + * Matcher logic for a Kotlin [KFunction] object. Also applicable to constructors. + */ +private class KFunctionMatcher( + private val name: Matcher, + private val returnType: Matcher, + vararg parameters: Matcher +) : DiagnosingMatcher>() { + private val parameters = listOf(*parameters) + + override fun describeTo(description: Description) { + description.appendText("KFunction[name as ").appendDescriptionOf(name) + .appendText(", returnType as ").appendDescriptionOf(returnType) + .appendText(", parameters as '") + if (parameters.isNotEmpty()) { + val param = parameters.iterator() + description.appendValue(param.next()) + while (param.hasNext()) { + description.appendText(",").appendValue(param.next()) + } + } + description.appendText("']") + } + + override fun matches(obj: Any?, mismatch: Description): Boolean { + if (obj == null) { + mismatch.appendText("is null") + return false + } + + val function: KFunction<*> = obj as? KFunction<*> ?: return false + if (!name.matches(function.name)) { + mismatch.appendText("name is ").appendValue(function.name) + return false + } + function.returnType.toString().apply { + if (!returnType.matches(this)) { + mismatch.appendText("returnType is ").appendValue(this) + return false + } + } + + if (function.valueParameters.size != parameters.size) { + mismatch.appendText("number of parameters is ").appendValue(function.valueParameters.size) + .appendText(", parameters=").appendValueList("[", ",", "]", function.valueParameters) + return false + } + + var i = 0 + function.valueParameters.forEach { param -> + if (!parameters[i].matches(param)) { + mismatch.appendText("parameter[").appendValue(i).appendText("] is ").appendValue(param) + return false + } + ++i + } + return true + } +} + +/** + * Matcher logic for a Kotlin [KParameter] object. + */ +private class KParameterMatcher( + private val type: Matcher +) : DiagnosingMatcher() { + override fun describeTo(description: Description) { + description.appendText("KParameter[type as ").appendDescriptionOf(type) + .appendText("]") + } + + override fun matches(obj: Any?, mismatch: Description): Boolean { + if (obj == null) { + mismatch.appendText("is null") + return false + } + + val parameter: KParameter = obj as? KParameter ?: return false + parameter.type.toString().apply { + if (!type.matches(this)) { + mismatch.appendText("type is ").appendValue(this) + return false + } + } + return true + } +} + +/** + * Matcher logic for a Kotlin [KProperty] object. + */ +private class KPropertyMatcher( + private val name: Matcher, + private val type: Matcher +) : DiagnosingMatcher>() { + override fun describeTo(description: Description) { + description.appendText("KProperty[name as ").appendDescriptionOf(name) + .appendText(", type as ").appendDescriptionOf(type) + .appendText("]") + } + + override fun matches(obj: Any?, mismatch: Description): Boolean { + if (obj == null) { + mismatch.appendText("is null") + return false + } + + val property: KProperty<*> = obj as? KProperty<*> ?: return false + if (!name.matches(property.name)) { + mismatch.appendText("name is ").appendValue(property.name) + return false + } + property.returnType.toString().apply { + if (!type.matches(this)) { + mismatch.appendText("type is ").appendValue(this) + return false + } + } + return true + } +} + +/** + * Matcher logic for a Kotlin [KClass] object. + */ +private class KClassMatcher(private val className: Matcher) : DiagnosingMatcher>() { + override fun describeTo(description: Description) { + description.appendText("KClass[name as ").appendDescriptionOf(className) + .appendText("]") + } + + override fun matches(obj: Any?, mismatch: Description): Boolean { + if (obj == null) { + mismatch.appendText("is null") + return false + } + + val type: KClass<*> = obj as? KClass<*> ?: return false + type.jvmName.apply { + if (!className.matches(this)) { + mismatch.appendText("name is ").appendValue(this) + return false + } + } + return true + } +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/resources/abstract-function/build.gradle b/buildSrc/jarfilter/src/test/resources/abstract-function/build.gradle new file mode 100644 index 0000000000..c67c80b303 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/abstract-function/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/abstract-function/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + compileOnly files('../../unwanteds/build/libs/unwanteds.jar') +} + +jar { + baseName = 'abstract-function' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forDelete = ["net.corda.gradle.jarfilter.DeleteMe"] + forStub = ["net.corda.gradle.jarfilter.StubMeOut"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/abstract-function/kotlin/net/corda/gradle/AbstractFunctions.kt b/buildSrc/jarfilter/src/test/resources/abstract-function/kotlin/net/corda/gradle/AbstractFunctions.kt new file mode 100644 index 0000000000..8d22e90854 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/abstract-function/kotlin/net/corda/gradle/AbstractFunctions.kt @@ -0,0 +1,13 @@ +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe +import net.corda.gradle.jarfilter.StubMeOut + +abstract class AbstractFunctions { + @DeleteMe + abstract fun toDelete(value: Long): Long + + @StubMeOut + abstract fun toStubOut(value: Long): Long +} diff --git a/buildSrc/jarfilter/src/test/resources/annotations/kotlin/net/corda/gradle/jarfilter/DeleteMe.kt b/buildSrc/jarfilter/src/test/resources/annotations/kotlin/net/corda/gradle/jarfilter/DeleteMe.kt new file mode 100644 index 0000000000..60ea95420b --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/annotations/kotlin/net/corda/gradle/jarfilter/DeleteMe.kt @@ -0,0 +1,20 @@ +package net.corda.gradle.jarfilter + +import kotlin.annotation.AnnotationRetention.* +import kotlin.annotation.AnnotationTarget.* +import kotlin.annotation.Retention +import kotlin.annotation.Target + +@Target( + FILE, + CLASS, + CONSTRUCTOR, + FUNCTION, + PROPERTY, + PROPERTY_GETTER, + PROPERTY_SETTER, + FIELD, + TYPEALIAS +) +@Retention(BINARY) +annotation class DeleteMe \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/resources/annotations/kotlin/net/corda/gradle/jarfilter/RemoveMe.kt b/buildSrc/jarfilter/src/test/resources/annotations/kotlin/net/corda/gradle/jarfilter/RemoveMe.kt new file mode 100644 index 0000000000..5023cff43a --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/annotations/kotlin/net/corda/gradle/jarfilter/RemoveMe.kt @@ -0,0 +1,19 @@ +package net.corda.gradle.jarfilter + +import kotlin.annotation.AnnotationRetention.* +import kotlin.annotation.AnnotationTarget.* +import kotlin.annotation.Retention +import kotlin.annotation.Target + +@Target( + FILE, + CLASS, + CONSTRUCTOR, + FUNCTION, + PROPERTY, + PROPERTY_GETTER, + PROPERTY_SETTER, + FIELD +) +@Retention(RUNTIME) +annotation class RemoveMe diff --git a/buildSrc/jarfilter/src/test/resources/annotations/kotlin/net/corda/gradle/jarfilter/StubMeOut.kt b/buildSrc/jarfilter/src/test/resources/annotations/kotlin/net/corda/gradle/jarfilter/StubMeOut.kt new file mode 100644 index 0000000000..5d148dcb58 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/annotations/kotlin/net/corda/gradle/jarfilter/StubMeOut.kt @@ -0,0 +1,15 @@ +package net.corda.gradle.jarfilter + +import kotlin.annotation.AnnotationRetention.* +import kotlin.annotation.AnnotationTarget.* +import kotlin.annotation.Retention +import kotlin.annotation.Target + +@Target( + CONSTRUCTOR, + FUNCTION, + PROPERTY_GETTER, + PROPERTY_SETTER +) +@Retention(RUNTIME) +annotation class StubMeOut \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/resources/delete-and-stub/build.gradle b/buildSrc/jarfilter/src/test/resources/delete-and-stub/build.gradle new file mode 100644 index 0000000000..8e356b4936 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-and-stub/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/delete-and-stub/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + compileOnly files('../../unwanteds/build/libs/unwanteds.jar') +} + +jar { + baseName = 'delete-and-stub' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forDelete = ["net.corda.gradle.jarfilter.DeleteMe"] + forStub = ["net.corda.gradle.jarfilter.StubMeOut"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-and-stub/kotlin/net/corda/gradle/DeletePackageWithStubbed.kt b/buildSrc/jarfilter/src/test/resources/delete-and-stub/kotlin/net/corda/gradle/DeletePackageWithStubbed.kt new file mode 100644 index 0000000000..b7d4baece2 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-and-stub/kotlin/net/corda/gradle/DeletePackageWithStubbed.kt @@ -0,0 +1,12 @@ +@file:JvmName("DeletePackageWithStubbed") +@file:Suppress("UNUSED") +@file:DeleteMe +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe +import net.corda.gradle.jarfilter.StubMeOut + +fun bracket(str: String): String = "[$str]" + +@StubMeOut +fun stubbed(str: String): String = bracket(str) diff --git a/buildSrc/jarfilter/src/test/resources/delete-and-stub/kotlin/net/corda/gradle/HasDeletedInsideStubbed.kt b/buildSrc/jarfilter/src/test/resources/delete-and-stub/kotlin/net/corda/gradle/HasDeletedInsideStubbed.kt new file mode 100644 index 0000000000..3083c2b4d8 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-and-stub/kotlin/net/corda/gradle/HasDeletedInsideStubbed.kt @@ -0,0 +1,28 @@ +@file:JvmName("HasDeletedInsideStubbed") +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe +import net.corda.gradle.jarfilter.StubMeOut +import net.corda.gradle.unwanted.HasString +import net.corda.gradle.unwanted.HasUnwantedFun +import net.corda.gradle.unwanted.HasUnwantedVal +import net.corda.gradle.unwanted.HasUnwantedVar + +class DeletedFunctionInsideStubbed(private val data: String): HasString, HasUnwantedFun { + @DeleteMe + override fun unwantedFun(str: String): String = str + + @StubMeOut + override fun stringData(): String = unwantedFun(data) +} + +class DeletedValInsideStubbed(@DeleteMe override val unwantedVal: String): HasString, HasUnwantedVal { + @StubMeOut + override fun stringData(): String = unwantedVal +} + +class DeletedVarInsideStubbed(@DeleteMe override var unwantedVar: String) : HasString, HasUnwantedVar { + @StubMeOut + override fun stringData(): String = unwantedVar +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/resources/delete-and-stub/kotlin/net/corda/gradle/HasPropertyForDeleteAndStub.kt b/buildSrc/jarfilter/src/test/resources/delete-and-stub/kotlin/net/corda/gradle/HasPropertyForDeleteAndStub.kt new file mode 100644 index 0000000000..c40d183929 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-and-stub/kotlin/net/corda/gradle/HasPropertyForDeleteAndStub.kt @@ -0,0 +1,21 @@ +@file:JvmName("HasPropertyForDeleteAndStub") +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe +import net.corda.gradle.jarfilter.StubMeOut +import net.corda.gradle.unwanted.* + +class HasVarPropertyForDeleteAndStub(value: Long) : HasLongVar { + @DeleteMe + @get:StubMeOut + @set:StubMeOut + override var longVar: Long = value +} + +class HasValPropertyForDeleteAndStub(str: String) : HasStringVal { + @DeleteMe + @get:StubMeOut + override val stringVal: String = str +} + diff --git a/buildSrc/jarfilter/src/test/resources/delete-constructor/build.gradle b/buildSrc/jarfilter/src/test/resources/delete-constructor/build.gradle new file mode 100644 index 0000000000..8503573c6c --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-constructor/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/delete-constructor/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + compileOnly files('../../unwanteds/build/libs/unwanteds.jar') +} + +jar { + baseName = 'delete-constructor' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forDelete = ["net.corda.gradle.jarfilter.DeleteMe"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-constructor/kotlin/net/corda/gradle/HasConstructorToDelete.kt b/buildSrc/jarfilter/src/test/resources/delete-constructor/kotlin/net/corda/gradle/HasConstructorToDelete.kt new file mode 100644 index 0000000000..6e142646d1 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-constructor/kotlin/net/corda/gradle/HasConstructorToDelete.kt @@ -0,0 +1,15 @@ +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe +import net.corda.gradle.unwanted.HasAll + +class HasConstructorToDelete(private val message: String, private val data: Long) : HasAll { + @DeleteMe constructor(message: String) : this(message, 0) + @DeleteMe constructor(data: Long) : this("", data) + constructor(data: Int) : this("", data.toLong()) + + override fun stringData(): String = message + override fun longData(): Long = data + override fun intData(): Int = data.toInt() +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-constructor/kotlin/net/corda/gradle/PrimaryConstructorsToDelete.kt b/buildSrc/jarfilter/src/test/resources/delete-constructor/kotlin/net/corda/gradle/PrimaryConstructorsToDelete.kt new file mode 100644 index 0000000000..4be16cd208 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-constructor/kotlin/net/corda/gradle/PrimaryConstructorsToDelete.kt @@ -0,0 +1,21 @@ +@file:JvmName("PrimaryConstructorsToDelete") +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe +import net.corda.gradle.unwanted.HasInt +import net.corda.gradle.unwanted.HasLong +import net.corda.gradle.unwanted.HasString + +class PrimaryIntConstructorToDelete @DeleteMe constructor(private val value: Int) : HasInt { + override fun intData() = value +} + +class PrimaryLongConstructorToDelete @DeleteMe constructor(private val value: Long) : HasLong { + override fun longData() = value +} + +class PrimaryStringConstructorToDelete @DeleteMe constructor(private val value: String) : HasString { + override fun stringData() = value +} + diff --git a/buildSrc/jarfilter/src/test/resources/delete-extension-val/build.gradle b/buildSrc/jarfilter/src/test/resources/delete-extension-val/build.gradle new file mode 100644 index 0000000000..2bc54db421 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-extension-val/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/delete-extension-val/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + compileOnly files('../../unwanteds/build/libs/unwanteds.jar') +} + +jar { + baseName = 'delete-extension-val' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forDelete = ["net.corda.gradle.jarfilter.DeleteMe"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-extension-val/kotlin/net/corda/gradle/HasValExtension.kt b/buildSrc/jarfilter/src/test/resources/delete-extension-val/kotlin/net/corda/gradle/HasValExtension.kt new file mode 100644 index 0000000000..f1219ab1ff --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-extension-val/kotlin/net/corda/gradle/HasValExtension.kt @@ -0,0 +1,10 @@ +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe +import net.corda.gradle.unwanted.HasUnwantedVal + +class HasValExtension(override val unwantedVal: String) : HasUnwantedVal { + @DeleteMe + val List.unwantedVal: String get() = this[0] +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-field/build.gradle b/buildSrc/jarfilter/src/test/resources/delete-field/build.gradle new file mode 100644 index 0000000000..92dd13a5f2 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-field/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/delete-field/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' +} + +jar { + baseName = 'delete-field' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forDelete = ["net.corda.gradle.jarfilter.DeleteMe"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-field/kotlin/net/corda/gradle/HasFieldToDelete.kt b/buildSrc/jarfilter/src/test/resources/delete-field/kotlin/net/corda/gradle/HasFieldToDelete.kt new file mode 100644 index 0000000000..d3127ce7d7 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-field/kotlin/net/corda/gradle/HasFieldToDelete.kt @@ -0,0 +1,23 @@ +@file:JvmName("HasFieldToDelete") +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe + +class HasStringFieldToDelete(value: String) { + @JvmField + @field:DeleteMe + val stringField: String = value +} + +class HasLongFieldToDelete(value: Long) { + @JvmField + @field:DeleteMe + val longField: Long = value +} + +class HasIntFieldToDelete(value: Int) { + @JvmField + @field:DeleteMe + val intField: Int = value +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-file-typealias/build.gradle b/buildSrc/jarfilter/src/test/resources/delete-file-typealias/build.gradle new file mode 100644 index 0000000000..f4de2588c8 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-file-typealias/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/delete-file-typealias/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' +} + +jar { + baseName = 'delete-file-typealias' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forDelete = ["net.corda.gradle.jarfilter.DeleteMe"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-file-typealias/kotlin/net/corda/gradle/FileWithTypeAlias.kt b/buildSrc/jarfilter/src/test/resources/delete-file-typealias/kotlin/net/corda/gradle/FileWithTypeAlias.kt new file mode 100644 index 0000000000..500e6b9be8 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-file-typealias/kotlin/net/corda/gradle/FileWithTypeAlias.kt @@ -0,0 +1,12 @@ +@file:JvmName("FileWithTypeAlias") +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe + +typealias FileWantedType = Long + +@DeleteMe +typealias FileUnwantedType = (String) -> Boolean + +val Any.FileUnwantedType: String get() = "" diff --git a/buildSrc/jarfilter/src/test/resources/delete-function/build.gradle b/buildSrc/jarfilter/src/test/resources/delete-function/build.gradle new file mode 100644 index 0000000000..5a5de263ad --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-function/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/delete-function/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + compileOnly files('../../unwanteds/build/libs/unwanteds.jar') +} + +jar { + baseName = 'delete-function' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forDelete = ["net.corda.gradle.jarfilter.DeleteMe"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-function/kotlin/net/corda/gradle/HasFunctionToDelete.kt b/buildSrc/jarfilter/src/test/resources/delete-function/kotlin/net/corda/gradle/HasFunctionToDelete.kt new file mode 100644 index 0000000000..81997e0857 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-function/kotlin/net/corda/gradle/HasFunctionToDelete.kt @@ -0,0 +1,12 @@ +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe +import net.corda.gradle.unwanted.HasUnwantedFun + +class HasFunctionToDelete : HasUnwantedFun { + @DeleteMe + override fun unwantedFun(str: String): String { + return str + } +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-function/kotlin/net/corda/gradle/HasIndirectFunctionToDelete.kt b/buildSrc/jarfilter/src/test/resources/delete-function/kotlin/net/corda/gradle/HasIndirectFunctionToDelete.kt new file mode 100644 index 0000000000..5a85f74d8e --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-function/kotlin/net/corda/gradle/HasIndirectFunctionToDelete.kt @@ -0,0 +1,13 @@ +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe +import net.corda.gradle.unwanted.HasString +import net.corda.gradle.unwanted.HasUnwantedFun + +class HasIndirectFunctionToDelete(private val data: String) : HasUnwantedFun, HasString { + @DeleteMe + override fun unwantedFun(str: String): String = str + + override fun stringData() = unwantedFun(data) +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/resources/delete-lazy/build.gradle b/buildSrc/jarfilter/src/test/resources/delete-lazy/build.gradle new file mode 100644 index 0000000000..74de278f40 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-lazy/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/delete-lazy/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + compileOnly files('../../unwanteds/build/libs/unwanteds.jar') +} + +jar { + baseName = 'delete-lazy' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forDelete = ["net.corda.gradle.jarfilter.DeleteMe"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-lazy/kotlin/net/corda/gradle/HasLazy.kt b/buildSrc/jarfilter/src/test/resources/delete-lazy/kotlin/net/corda/gradle/HasLazy.kt new file mode 100644 index 0000000000..ad824cab4d --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-lazy/kotlin/net/corda/gradle/HasLazy.kt @@ -0,0 +1,13 @@ +@file:JvmName("HasLazy") +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe +import net.corda.gradle.unwanted.HasUnwantedVal + +class HasLazyVal(private val message: String) : HasUnwantedVal { + @DeleteMe + override val unwantedVal: String by lazy { + message + } +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-multifile/build.gradle b/buildSrc/jarfilter/src/test/resources/delete-multifile/build.gradle new file mode 100644 index 0000000000..417c8d97fe --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-multifile/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/delete-multifile/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' +} + +jar { + baseName = 'delete-multifile' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forDelete = ["net.corda.gradle.jarfilter.DeleteMe"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-multifile/kotlin/net/corda/gradle/HasInt.kt b/buildSrc/jarfilter/src/test/resources/delete-multifile/kotlin/net/corda/gradle/HasInt.kt new file mode 100644 index 0000000000..0c4507d568 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-multifile/kotlin/net/corda/gradle/HasInt.kt @@ -0,0 +1,9 @@ +@file:JvmName("HasMultiData") +@file:JvmMultifileClass +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe + +@DeleteMe +fun intToDelete(data: Int): Int = data diff --git a/buildSrc/jarfilter/src/test/resources/delete-multifile/kotlin/net/corda/gradle/HasLong.kt b/buildSrc/jarfilter/src/test/resources/delete-multifile/kotlin/net/corda/gradle/HasLong.kt new file mode 100644 index 0000000000..c4c6617e33 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-multifile/kotlin/net/corda/gradle/HasLong.kt @@ -0,0 +1,9 @@ +@file:JvmName("HasMultiData") +@file:JvmMultifileClass +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe + +@DeleteMe +fun longToDelete(data: Long): Long = data diff --git a/buildSrc/jarfilter/src/test/resources/delete-multifile/kotlin/net/corda/gradle/HasString.kt b/buildSrc/jarfilter/src/test/resources/delete-multifile/kotlin/net/corda/gradle/HasString.kt new file mode 100644 index 0000000000..3fe9965c2f --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-multifile/kotlin/net/corda/gradle/HasString.kt @@ -0,0 +1,9 @@ +@file:JvmName("HasMultiData") +@file:JvmMultifileClass +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe + +@DeleteMe +fun stringToDelete(str: String): String = str diff --git a/buildSrc/jarfilter/src/test/resources/delete-nested-class/build.gradle b/buildSrc/jarfilter/src/test/resources/delete-nested-class/build.gradle new file mode 100644 index 0000000000..9a74a8e3c3 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-nested-class/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/delete-nested-class/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' +} + +jar { + baseName = 'delete-nested-class' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forDelete = ["net.corda.gradle.jarfilter.DeleteMe"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-nested-class/kotlin/net/corda/gradle/HasNestedClasses.kt b/buildSrc/jarfilter/src/test/resources/delete-nested-class/kotlin/net/corda/gradle/HasNestedClasses.kt new file mode 100644 index 0000000000..a8f8a2f2f4 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-nested-class/kotlin/net/corda/gradle/HasNestedClasses.kt @@ -0,0 +1,10 @@ +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe + +class HasNestedClasses { + class OneToKeep + + @DeleteMe class OneToThrowAway +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-nested-class/kotlin/net/corda/gradle/SealedClass.kt b/buildSrc/jarfilter/src/test/resources/delete-nested-class/kotlin/net/corda/gradle/SealedClass.kt new file mode 100644 index 0000000000..1e85d6564c --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-nested-class/kotlin/net/corda/gradle/SealedClass.kt @@ -0,0 +1,10 @@ +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe + +sealed class SealedClass { + class Wanted : SealedClass() + + @DeleteMe class Unwanted : SealedClass() +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-object/build.gradle b/buildSrc/jarfilter/src/test/resources/delete-object/build.gradle new file mode 100644 index 0000000000..9958740aad --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-object/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/delete-object/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + compileOnly files('../../unwanteds/build/libs/unwanteds.jar') +} + +jar { + baseName = 'delete-object' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forDelete = ["net.corda.gradle.jarfilter.DeleteMe"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-object/kotlin/net/corda/gradle/HasObjects.kt b/buildSrc/jarfilter/src/test/resources/delete-object/kotlin/net/corda/gradle/HasObjects.kt new file mode 100644 index 0000000000..03ec14bcb4 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-object/kotlin/net/corda/gradle/HasObjects.kt @@ -0,0 +1,20 @@ +@file:JvmName("HasObjects") +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe +import net.corda.gradle.unwanted.HasUnwantedFun +import net.corda.gradle.unwanted.HasUnwantedVal + +@DeleteMe +val unwantedObj = object : HasUnwantedFun { + override fun unwantedFun(str: String): String = str +} + +@DeleteMe +fun unwantedFun(): String { + val obj = object : HasUnwantedVal { + override val unwantedVal: String = "" + } + return obj.unwantedVal +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-sealed-subclass/build.gradle b/buildSrc/jarfilter/src/test/resources/delete-sealed-subclass/build.gradle new file mode 100644 index 0000000000..38c76d6f7c --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-sealed-subclass/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/delete-sealed-subclass/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + compileOnly files('../../unwanteds/build/libs/unwanteds.jar') +} + +jar { + baseName = 'delete-sealed-subclass' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forDelete = ["net.corda.gradle.jarfilter.DeleteMe"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-sealed-subclass/kotlin/net/corda/gradle/SealedWithSubclasses.kt b/buildSrc/jarfilter/src/test/resources/delete-sealed-subclass/kotlin/net/corda/gradle/SealedWithSubclasses.kt new file mode 100644 index 0000000000..52e28a4475 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-sealed-subclass/kotlin/net/corda/gradle/SealedWithSubclasses.kt @@ -0,0 +1,12 @@ +@file:JvmName("SealedWithSubclasses") +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe + +sealed class SealedBaseClass + +@DeleteMe +class UnwantedSubclass : SealedBaseClass() + +class WantedSubclass : SealedBaseClass() diff --git a/buildSrc/jarfilter/src/test/resources/delete-static-field/build.gradle b/buildSrc/jarfilter/src/test/resources/delete-static-field/build.gradle new file mode 100644 index 0000000000..18040b9954 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-static-field/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/delete-static-field/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' +} + +jar { + baseName = 'delete-static-field' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forDelete = ["net.corda.gradle.jarfilter.DeleteMe"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-static-field/kotlin/net/corda/gradle/StaticFieldsToDelete.kt b/buildSrc/jarfilter/src/test/resources/delete-static-field/kotlin/net/corda/gradle/StaticFieldsToDelete.kt new file mode 100644 index 0000000000..a16350d5d4 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-static-field/kotlin/net/corda/gradle/StaticFieldsToDelete.kt @@ -0,0 +1,17 @@ +@file:JvmName("StaticFieldsToDelete") +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe + +@DeleteMe +@JvmField +val stringField: String = "" + +@DeleteMe +@JvmField +val longField: Long = 123456789L + +@DeleteMe +@JvmField +val intField: Int = 123456 diff --git a/buildSrc/jarfilter/src/test/resources/delete-static-function/build.gradle b/buildSrc/jarfilter/src/test/resources/delete-static-function/build.gradle new file mode 100644 index 0000000000..b2308fad37 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-static-function/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/delete-static-function/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + compileOnly files('../../unwanteds/build/libs/unwanteds.jar') +} + +jar { + baseName = 'delete-static-function' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forDelete = ["net.corda.gradle.jarfilter.DeleteMe"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-static-function/kotlin/net/corda/gradle/StaticFunctionsToDelete.kt b/buildSrc/jarfilter/src/test/resources/delete-static-function/kotlin/net/corda/gradle/StaticFunctionsToDelete.kt new file mode 100644 index 0000000000..a9144140c0 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-static-function/kotlin/net/corda/gradle/StaticFunctionsToDelete.kt @@ -0,0 +1,14 @@ +@file:JvmName("StaticFunctionsToDelete") +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe + +@DeleteMe +fun unwantedStringToDelete(value: String): String = value + +@DeleteMe +fun unwantedIntToDelete(value: Int): Int = value + +@DeleteMe +fun unwantedLongToDelete(value: Long): Long = value diff --git a/buildSrc/jarfilter/src/test/resources/delete-static-val/build.gradle b/buildSrc/jarfilter/src/test/resources/delete-static-val/build.gradle new file mode 100644 index 0000000000..a67c4f7760 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-static-val/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/delete-static-val/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' +} + +jar { + baseName = 'delete-static-val' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forDelete = ["net.corda.gradle.jarfilter.DeleteMe"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-static-val/kotlin/net/corda/gradle/StaticValToDelete.kt b/buildSrc/jarfilter/src/test/resources/delete-static-val/kotlin/net/corda/gradle/StaticValToDelete.kt new file mode 100644 index 0000000000..51de79e5c5 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-static-val/kotlin/net/corda/gradle/StaticValToDelete.kt @@ -0,0 +1,17 @@ +@file:JvmName("StaticValToDelete") +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe + +@DeleteMe +val stringVal: String = "" + +@DeleteMe +val longVal: Long = 123456789L + +@DeleteMe +val intVal: Int = 123456 + +@DeleteMe +val T.memberVal: T get() = this diff --git a/buildSrc/jarfilter/src/test/resources/delete-static-var/build.gradle b/buildSrc/jarfilter/src/test/resources/delete-static-var/build.gradle new file mode 100644 index 0000000000..2d601a37c4 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-static-var/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/delete-static-var/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' +} + +jar { + baseName = 'delete-static-var' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forDelete = ["net.corda.gradle.jarfilter.DeleteMe"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-static-var/kotlin/net/corda/gradle/StaticVarToDelete.kt b/buildSrc/jarfilter/src/test/resources/delete-static-var/kotlin/net/corda/gradle/StaticVarToDelete.kt new file mode 100644 index 0000000000..1b941b8fff --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-static-var/kotlin/net/corda/gradle/StaticVarToDelete.kt @@ -0,0 +1,19 @@ +@file:JvmName("StaticVarToDelete") +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe + +@DeleteMe +var stringVar: String = "" + +@DeleteMe +var longVar: Long = 123456789L + +@DeleteMe +var intVar: Int = 123456 + +@DeleteMe +var T.memberVar: T + get() = this + set(value) { } diff --git a/buildSrc/jarfilter/src/test/resources/delete-val-property/build.gradle b/buildSrc/jarfilter/src/test/resources/delete-val-property/build.gradle new file mode 100644 index 0000000000..3d6f3d4224 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-val-property/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/delete-val-property/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + compileOnly files('../../unwanteds/build/libs/unwanteds.jar') +} + +jar { + baseName = 'delete-val-property' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forDelete = ["net.corda.gradle.jarfilter.DeleteMe"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-val-property/kotlin/net/corda/gradle/HasValPropertyForDelete.kt b/buildSrc/jarfilter/src/test/resources/delete-val-property/kotlin/net/corda/gradle/HasValPropertyForDelete.kt new file mode 100644 index 0000000000..26f9ebfad3 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-val-property/kotlin/net/corda/gradle/HasValPropertyForDelete.kt @@ -0,0 +1,11 @@ +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe +import net.corda.gradle.unwanted.HasUnwantedVal + +class HasValPropertyForDelete(@DeleteMe override val unwantedVal: String) : HasUnwantedVal + +class HasValGetterForDelete(@get:DeleteMe override val unwantedVal: String): HasUnwantedVal + +class HasValJvmFieldForDelete(@DeleteMe @JvmField val unwantedVal: String) \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/resources/delete-var-property/build.gradle b/buildSrc/jarfilter/src/test/resources/delete-var-property/build.gradle new file mode 100644 index 0000000000..dfc29b78b6 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-var-property/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/delete-var-property/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + compileOnly files('../../unwanteds/build/libs/unwanteds.jar') +} + +jar { + baseName = 'delete-var-property' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forDelete = ["net.corda.gradle.jarfilter.DeleteMe"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-var-property/kotlin/net/corda/gradle/HasVarPropertyForDelete.kt b/buildSrc/jarfilter/src/test/resources/delete-var-property/kotlin/net/corda/gradle/HasVarPropertyForDelete.kt new file mode 100644 index 0000000000..4726ef9e3a --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-var-property/kotlin/net/corda/gradle/HasVarPropertyForDelete.kt @@ -0,0 +1,14 @@ +@file:JvmName("HasVarPropertyForDelete") +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe +import net.corda.gradle.unwanted.HasUnwantedVar + +class HasUnwantedVarPropertyForDelete(@DeleteMe override var unwantedVar: String) : HasUnwantedVar + +class HasUnwantedGetForDelete(@get:DeleteMe override var unwantedVar: String) : HasUnwantedVar + +class HasUnwantedSetForDelete(@set:DeleteMe override var unwantedVar: String) : HasUnwantedVar + +class HasVarJvmFieldForDelete(@DeleteMe @JvmField var unwantedVar: String) diff --git a/buildSrc/jarfilter/src/test/resources/gradle.properties b/buildSrc/jarfilter/src/test/resources/gradle.properties new file mode 100644 index 0000000000..0c744a8a05 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/gradle.properties @@ -0,0 +1 @@ +org.gradle.jvmargs=-javaagent:"$jacocoAgent"=destfile="$buildDir/jacoco/test.exec",includes=net/corda/gradle/jarfilter/** diff --git a/buildSrc/jarfilter/src/test/resources/interface-function/build.gradle b/buildSrc/jarfilter/src/test/resources/interface-function/build.gradle new file mode 100644 index 0000000000..dbea447e6b --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/interface-function/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/interface-function/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + compileOnly files('../../unwanteds/build/libs/unwanteds.jar') +} + +jar { + baseName = 'interface-function' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forDelete = ["net.corda.gradle.jarfilter.DeleteMe"] + forStub = ["net.corda.gradle.jarfilter.StubMeOut"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/interface-function/kotlin/net/corda/gradle/InterfaceFunctions.kt b/buildSrc/jarfilter/src/test/resources/interface-function/kotlin/net/corda/gradle/InterfaceFunctions.kt new file mode 100644 index 0000000000..cbc782794a --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/interface-function/kotlin/net/corda/gradle/InterfaceFunctions.kt @@ -0,0 +1,13 @@ +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe +import net.corda.gradle.jarfilter.StubMeOut + +interface InterfaceFunctions { + @DeleteMe + fun toDelete(value: Long): Long + + @StubMeOut + fun toStubOut(value: Long): Long +} diff --git a/buildSrc/jarfilter/src/test/resources/remove-annotations/build.gradle b/buildSrc/jarfilter/src/test/resources/remove-annotations/build.gradle new file mode 100644 index 0000000000..e477038ce9 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/remove-annotations/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/remove-annotations/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + compileOnly files('../../unwanteds/build/libs/unwanteds.jar') +} + +jar { + baseName = 'remove-annotations' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forRemove = ["net.corda.gradle.jarfilter.RemoveMe"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/remove-annotations/kotlin/net/corda/gradle/HasUnwantedAnnotations.kt b/buildSrc/jarfilter/src/test/resources/remove-annotations/kotlin/net/corda/gradle/HasUnwantedAnnotations.kt new file mode 100644 index 0000000000..e0732943ee --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/remove-annotations/kotlin/net/corda/gradle/HasUnwantedAnnotations.kt @@ -0,0 +1,33 @@ +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.RemoveMe +import net.corda.gradle.unwanted.HasUnwantedFun +import net.corda.gradle.unwanted.HasUnwantedVal +import net.corda.gradle.unwanted.HasUnwantedVar + +@RemoveMe +class HasUnwantedAnnotations @RemoveMe constructor( + longValue: Long, message: String +) : HasUnwantedVar, HasUnwantedVal, HasUnwantedFun { + @RemoveMe + constructor() : this(999L, "") + + @field:RemoveMe + @JvmField + val longField: Long = longValue + + @get:RemoveMe + @property:RemoveMe + override val unwantedVal: String = message + + @get:RemoveMe + @set:RemoveMe + @property:RemoveMe + override var unwantedVar: String = message + + @RemoveMe + override fun unwantedFun(str: String): String { + return "[$str]" + } +} diff --git a/buildSrc/jarfilter/src/test/resources/repositories.gradle b/buildSrc/jarfilter/src/test/resources/repositories.gradle new file mode 100644 index 0000000000..2a25f5bbd4 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/repositories.gradle @@ -0,0 +1,4 @@ +repositories { + mavenLocal() + jcenter() +} diff --git a/buildSrc/jarfilter/src/test/resources/settings.gradle b/buildSrc/jarfilter/src/test/resources/settings.gradle new file mode 100644 index 0000000000..7e0a88c8e3 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/settings.gradle @@ -0,0 +1,6 @@ +// Common settings for all Gradle test projects. +pluginManagement { + repositories { + gradlePluginPortal() + } +} diff --git a/buildSrc/jarfilter/src/test/resources/stub-constructor/build.gradle b/buildSrc/jarfilter/src/test/resources/stub-constructor/build.gradle new file mode 100644 index 0000000000..857ce08d50 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/stub-constructor/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/stub-constructor/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + compileOnly files('../../unwanteds/build/libs/unwanteds.jar') +} + +jar { + baseName = 'stub-constructor' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forStub = ["net.corda.gradle.jarfilter.StubMeOut"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/stub-constructor/kotlin/net/corda/gradle/HasConstructorToStub.kt b/buildSrc/jarfilter/src/test/resources/stub-constructor/kotlin/net/corda/gradle/HasConstructorToStub.kt new file mode 100644 index 0000000000..5683a97243 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/stub-constructor/kotlin/net/corda/gradle/HasConstructorToStub.kt @@ -0,0 +1,15 @@ +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.StubMeOut +import net.corda.gradle.unwanted.HasAll + +class HasConstructorToStub(private val message: String, private val data: Long) : HasAll { + @StubMeOut constructor(message: String) : this(message, 0) + @StubMeOut constructor(data: Long) : this("", data) + constructor(data: Int) : this("", data.toLong()) + + override fun stringData(): String = message + override fun longData(): Long = data + override fun intData(): Int = data.toInt() +} diff --git a/buildSrc/jarfilter/src/test/resources/stub-constructor/kotlin/net/corda/gradle/PrimaryConstructorsToStub.kt b/buildSrc/jarfilter/src/test/resources/stub-constructor/kotlin/net/corda/gradle/PrimaryConstructorsToStub.kt new file mode 100644 index 0000000000..1e25467643 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/stub-constructor/kotlin/net/corda/gradle/PrimaryConstructorsToStub.kt @@ -0,0 +1,21 @@ +@file:JvmName("PrimaryConstructorsToStub") +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.StubMeOut +import net.corda.gradle.unwanted.HasInt +import net.corda.gradle.unwanted.HasLong +import net.corda.gradle.unwanted.HasString + +class PrimaryIntConstructorToStub @StubMeOut constructor(private val value: Int) : HasInt { + override fun intData() = value +} + +class PrimaryLongConstructorToStub @StubMeOut constructor(private val value: Long) : HasLong { + override fun longData() = value +} + +class PrimaryStringConstructorToStub @StubMeOut constructor(private val value: String) : HasString { + override fun stringData() = value +} + diff --git a/buildSrc/jarfilter/src/test/resources/stub-function/build.gradle b/buildSrc/jarfilter/src/test/resources/stub-function/build.gradle new file mode 100644 index 0000000000..98a3d6275a --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/stub-function/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/stub-function/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + compileOnly files('../../unwanteds/build/libs/unwanteds.jar') +} + +jar { + baseName = 'stub-function' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forStub = ["net.corda.gradle.jarfilter.StubMeOut"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/stub-function/kotlin/net/corda/gradle/HasFunctionToStub.kt b/buildSrc/jarfilter/src/test/resources/stub-function/kotlin/net/corda/gradle/HasFunctionToStub.kt new file mode 100644 index 0000000000..32c3a17533 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/stub-function/kotlin/net/corda/gradle/HasFunctionToStub.kt @@ -0,0 +1,14 @@ +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.StubMeOut +import net.corda.gradle.unwanted.HasUnwantedFun +import javax.annotation.Resource + +class HasFunctionToStub : HasUnwantedFun { + @StubMeOut + @Resource + override fun unwantedFun(@Parameter str: String): String { + return str + } +} diff --git a/buildSrc/jarfilter/src/test/resources/stub-function/kotlin/net/corda/gradle/RuntimeAnnotations.kt b/buildSrc/jarfilter/src/test/resources/stub-function/kotlin/net/corda/gradle/RuntimeAnnotations.kt new file mode 100644 index 0000000000..ef1e66b89b --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/stub-function/kotlin/net/corda/gradle/RuntimeAnnotations.kt @@ -0,0 +1,11 @@ +@file:JvmName("RuntimeAnnotations") +package net.corda.gradle + +import kotlin.annotation.AnnotationRetention.* +import kotlin.annotation.AnnotationTarget.* +import kotlin.annotation.Retention +import kotlin.annotation.Target + +@Target(VALUE_PARAMETER) +@Retention(RUNTIME) +annotation class Parameter diff --git a/buildSrc/jarfilter/src/test/resources/stub-static-function/build.gradle b/buildSrc/jarfilter/src/test/resources/stub-static-function/build.gradle new file mode 100644 index 0000000000..75eb1e2cfe --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/stub-static-function/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/stub-static-function/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + compileOnly files('../../unwanteds/build/libs/unwanteds.jar') +} + +jar { + baseName = 'stub-static-function' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forStub = ["net.corda.gradle.jarfilter.StubMeOut"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/stub-static-function/kotlin/net/corda/gradle/StaticFunctionsToStub.kt b/buildSrc/jarfilter/src/test/resources/stub-static-function/kotlin/net/corda/gradle/StaticFunctionsToStub.kt new file mode 100644 index 0000000000..ffeece6a04 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/stub-static-function/kotlin/net/corda/gradle/StaticFunctionsToStub.kt @@ -0,0 +1,22 @@ +@file:JvmName("StaticFunctionsToStub") +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.StubMeOut + +@StubMeOut +fun unwantedStringToStub(value: String): String = value + +@StubMeOut +fun unwantedIntToStub(value: Int): Int = value + +@StubMeOut +fun unwantedLongToStub(value: Long): Long = value + +private var seed: Int = 0 +val staticSeed: Int get() = seed + +@StubMeOut +fun unwantedVoidToStub() { + ++seed +} diff --git a/buildSrc/jarfilter/src/test/resources/stub-val-property/build.gradle b/buildSrc/jarfilter/src/test/resources/stub-val-property/build.gradle new file mode 100644 index 0000000000..e92e2b68c0 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/stub-val-property/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/stub-val-property/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + compileOnly files('../../unwanteds/build/libs/unwanteds.jar') +} + +jar { + baseName = 'stub-val-property' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forStub = ["net.corda.gradle.jarfilter.StubMeOut"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/stub-val-property/kotlin/net/corda/gradle/HasValPropertyForStub.kt b/buildSrc/jarfilter/src/test/resources/stub-val-property/kotlin/net/corda/gradle/HasValPropertyForStub.kt new file mode 100644 index 0000000000..b9a6c042b4 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/stub-val-property/kotlin/net/corda/gradle/HasValPropertyForStub.kt @@ -0,0 +1,7 @@ +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.StubMeOut +import net.corda.gradle.unwanted.HasUnwantedVal + +class HasValPropertyForStub(@get:StubMeOut override val unwantedVal: String) : HasUnwantedVal diff --git a/buildSrc/jarfilter/src/test/resources/stub-var-property/build.gradle b/buildSrc/jarfilter/src/test/resources/stub-var-property/build.gradle new file mode 100644 index 0000000000..d02be34534 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/stub-var-property/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/stub-var-property/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + compileOnly files('../../unwanteds/build/libs/unwanteds.jar') +} + +jar { + baseName = 'stub-var-property' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forStub = ["net.corda.gradle.jarfilter.StubMeOut"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/stub-var-property/kotlin/net/corda/gradle/HasVarPropertyForStub.kt b/buildSrc/jarfilter/src/test/resources/stub-var-property/kotlin/net/corda/gradle/HasVarPropertyForStub.kt new file mode 100644 index 0000000000..8f346142b0 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/stub-var-property/kotlin/net/corda/gradle/HasVarPropertyForStub.kt @@ -0,0 +1,10 @@ +@file:JvmName("HasVarPropertyForStub") +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.StubMeOut +import net.corda.gradle.unwanted.HasUnwantedVar + +class HasUnwantedGetForStub(@get:StubMeOut override var unwantedVar: String) : HasUnwantedVar + +class HasUnwantedSetForStub(@set:StubMeOut override var unwantedVar: String) : HasUnwantedVar \ No newline at end of file diff --git a/buildSrc/jarfilter/unwanteds/build.gradle b/buildSrc/jarfilter/unwanteds/build.gradle new file mode 100644 index 0000000000..0f2deeb1c6 --- /dev/null +++ b/buildSrc/jarfilter/unwanteds/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'kotlin' + +description 'Test artifacts for the jar-filter plugin.' + +repositories { + mavenLocal() + jcenter() +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" +} diff --git a/buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasData.kt b/buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasData.kt new file mode 100644 index 0000000000..87e52bbbf7 --- /dev/null +++ b/buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasData.kt @@ -0,0 +1,16 @@ +@file:JvmName("HasData") +package net.corda.gradle.unwanted + +interface HasString { + fun stringData(): String +} + +interface HasLong { + fun longData(): Long +} + +interface HasInt { + fun intData(): Int +} + +interface HasAll : HasInt, HasLong, HasString \ No newline at end of file diff --git a/buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasUnwantedFun.kt b/buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasUnwantedFun.kt new file mode 100644 index 0000000000..f66b53c8b7 --- /dev/null +++ b/buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasUnwantedFun.kt @@ -0,0 +1,5 @@ +package net.corda.gradle.unwanted + +interface HasUnwantedFun { + fun unwantedFun(str: String): String +} diff --git a/buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasUnwantedVal.kt b/buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasUnwantedVal.kt new file mode 100644 index 0000000000..b19d043345 --- /dev/null +++ b/buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasUnwantedVal.kt @@ -0,0 +1,5 @@ +package net.corda.gradle.unwanted + +interface HasUnwantedVal { + val unwantedVal: String +} diff --git a/buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasUnwantedVar.kt b/buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasUnwantedVar.kt new file mode 100644 index 0000000000..7406028cb8 --- /dev/null +++ b/buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasUnwantedVar.kt @@ -0,0 +1,5 @@ +package net.corda.gradle.unwanted + +interface HasUnwantedVar { + var unwantedVar: String +} diff --git a/buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasVal.kt b/buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasVal.kt new file mode 100644 index 0000000000..7aeb75ca35 --- /dev/null +++ b/buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasVal.kt @@ -0,0 +1,17 @@ +@file:JvmName("HasVal") +@file:Suppress("UNUSED") +package net.corda.gradle.unwanted + +interface HasStringVal { + val stringVal: String +} + +interface HasLongVal { + val longVal: Long +} + +interface HasIntVal { + val intVal: Int +} + +interface HasAllVal : HasIntVal, HasLongVal, HasStringVal \ No newline at end of file diff --git a/buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasVar.kt b/buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasVar.kt new file mode 100644 index 0000000000..74fa87062c --- /dev/null +++ b/buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasVar.kt @@ -0,0 +1,17 @@ +@file:JvmName("HasVar") +@file:Suppress("UNUSED") +package net.corda.gradle.unwanted + +interface HasStringVar { + var stringVar: String +} + +interface HasLongVar { + var longVar: Long +} + +interface HasIntVar { + var intVar: Int +} + +interface HasAllVar : HasIntVar, HasLongVar, HasStringVar diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle index ba2a28b8ad..4bd3f3acd2 100644 --- a/buildSrc/settings.gradle +++ b/buildSrc/settings.gradle @@ -10,3 +10,6 @@ rootProject.name = 'buildSrc' include 'canonicalizer' +include 'jarfilter' +include 'jarfilter:unwanteds' +include 'jarfilter:kotlin-metadata' diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt index dfb5f7f1fb..d6538d1ffc 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt @@ -22,10 +22,10 @@ class AMQPClientSerializationScheme( cordappCustomSerializers: Set>, serializerFactoriesForContexts: MutableMap, SerializerFactory> ) : AbstractAMQPSerializationScheme(cordappCustomSerializers, serializerFactoriesForContexts) { - constructor(cordapps: List) : this(cordapps.customSerializers, ConcurrentHashMap()) + constructor(cordapps: List) : this(cordapps.customSerializers, ConcurrentHashMap()) - @Suppress("UNUSED") - constructor() : this(emptySet(), ConcurrentHashMap()) + @Suppress("UNUSED") + constructor() : this(emptySet(), ConcurrentHashMap()) companion object { /** Call from main only. */ diff --git a/constants.properties b/constants.properties index 59e8955332..d8280be3be 100644 --- a/constants.properties +++ b/constants.properties @@ -8,10 +8,11 @@ # Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. # -gradlePluginsVersion=4.0.22 +gradlePluginsVersion=4.0.23 kotlinVersion=1.2.41 platformVersion=4 guavaVersion=21.0 +proguardVersion=6.0.3 bouncycastleVersion=1.57 typesafeConfigVersion=1.3.1 jsr305Version=3.0.2 diff --git a/core-deterministic/build.gradle b/core-deterministic/build.gradle new file mode 100644 index 0000000000..42d3a9f32a --- /dev/null +++ b/core-deterministic/build.gradle @@ -0,0 +1,211 @@ +/* + * R3 Proprietary and Confidential + * + * Copyright (c) 2018 R3 Limited. All rights reserved. + * + * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. + * + * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. + */ +description 'Corda core (deterministic)' + +apply plugin: 'kotlin' +apply plugin: 'com.jfrog.artifactory' +apply plugin: 'net.corda.plugins.publish-utils' + +evaluationDependsOn(':jdk8u-deterministic') +evaluationDependsOn(":core") + +def javaHome = System.getProperty('java.home') +def jarBaseName = "corda-${project.name}".toString() +def jdkTask = project(':jdk8u-deterministic').assemble +def deterministic_jdk_home = project(':jdk8u-deterministic').jdk_home + +configurations { + runtimeLibraries + runtimeArtifacts.extendsFrom runtimeLibraries +} + +dependencies { + compileOnly project(':core') + compileOnly "com.google.guava:guava:$guava_version" + compileOnly "$quasar_group:quasar-core:$quasar_version:jdk8" + + // Configure these by hand. It should be a minimal subset of core's dependencies, + // and without any obviously non-deterministic ones such as Hibernate. + runtimeLibraries "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + runtimeLibraries "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + runtimeLibraries "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final" + runtimeLibraries "org.bouncycastle:bcprov-jdk15on:$bouncycastle_version" + runtimeLibraries "org.bouncycastle:bcpkix-jdk15on:$bouncycastle_version" + runtimeLibraries "com.google.code.findbugs:jsr305:$jsr305_version" + runtimeLibraries "com.google.guava:guava:$guava_version" + runtimeLibraries "net.i2p.crypto:eddsa:$eddsa_version" + runtimeLibraries "org.slf4j:slf4j-api:$slf4j_version" +} + +tasks.withType(AbstractCompile) { + dependsOn jdkTask +} + +tasks.withType(JavaCompile) { + options.compilerArgs << '-bootclasspath' << "$deterministic_jdk_home/jre/lib/rt.jar".toString() +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) { + kotlinOptions.jdkHome = deterministic_jdk_home +} + +jar { + baseName 'DOES-NOT-EXIST' + // Don't build a jar here because it would be the wrong one. + // The jar we really want will be built by the metafix task. + enabled = false +} + +def coreJarTask = tasks.getByPath(':core:jar') +def originalJar = coreJarTask.outputs.files.singleFile + +task patchCore(type: Zip, dependsOn: coreJarTask) { + destinationDir file("$buildDir/source-libs") + metadataCharset 'UTF-8' + classifier 'transient' + extension 'jar' + + from(compileKotlin) + from(zipTree(originalJar)) { + exclude 'net/corda/core/crypto/DelegatingSecureRandomService*.class' + exclude 'net/corda/core/crypto/SHA256DigestSupplier.class' + exclude 'net/corda/core/internal/*ToggleField*.class' + exclude 'net/corda/core/serialization/*SerializationFactory*.class' + exclude 'net/corda/core/utilities/SgxSupport*.class' + } + + reproducibleFileOrder = true + includeEmptyDirs = false +} + +import proguard.gradle.ProGuardTask +task predeterminise(type: ProGuardTask) { + injars patchCore + outjars "$buildDir/proguard/pre-deterministic-${project.version}.jar" + + libraryjars "$javaHome/lib/rt.jar" + libraryjars "$javaHome/lib/jce.jar" + configurations.compileOnly.forEach { + if (originalJar.path != it.path) { + libraryjars it.path, filter: '!META-INF/versions/**' + } + } + + keepattributes '*' + keepdirectories + dontwarn '**$1$1' + dontpreverify + dontobfuscate + dontoptimize + printseeds + verbose + + keep '@interface net.corda.core.* { *; }' + keep '@interface net.corda.core.contracts.** { *; }' + keep '@interface net.corda.core.serialization.** { *; }' + keep '@net.corda.core.KeepForDJVM class * { *; }', includedescriptorclasses:true + keepclassmembers 'class net.corda.core.** { public synthetic ; }' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars predeterminise + annotations { + forDelete = [ + "net.corda.core.DeleteForDJVM" + ] + forStub = [ + "net.corda.core.StubOutForDJVM" + ] + forRemove = [ + "co.paralleluniverse.fibers.Suspendable" + ] + } +} + +task determinise(type: ProGuardTask) { + injars jarFilter + outjars "$buildDir/proguard/$jarBaseName-${project.version}.jar" + + libraryjars "$javaHome/lib/rt.jar" + libraryjars "$javaHome/lib/jce.jar" + configurations.runtimeLibraries.forEach { + libraryjars it.path, filter: '!META-INF/versions/**' + } + + // Analyse the JAR for dead code, and remove (some of) it. + optimizations 'code/removal/simple,code/removal/advanced' + printconfiguration + + keepattributes '*' + keepdirectories + dontobfuscate + printseeds + verbose + + keep '@interface net.corda.core.CordaInternal { *; }' + keep '@interface net.corda.core.DoNotImplement { *; }' + keep '@interface net.corda.core.KeepForDJVM { *; }' + keep '@interface net.corda.core.contracts.** { *; }' + keep '@interface net.corda.core.serialization.** { *; }' + keep '@net.corda.core.KeepForDJVM class * { *; }', includedescriptorclasses:true + keepclassmembers 'class net.corda.core.** { public synthetic ; }' +} + +import net.corda.gradle.jarfilter.MetaFixerTask +task metafix(type: MetaFixerTask) { + outputDir file("$buildDir/libs") + jars determinise + suffix "" + + // Strip timestamps from the JAR to make it reproducible. + preserveTimestamps = false +} + +// DOCSTART 01 +task checkDeterminism(type: ProGuardTask, dependsOn: jdkTask) { + injars metafix + + libraryjars "$deterministic_jdk_home/jre/lib/rt.jar" + + configurations.runtimeLibraries.forEach { + libraryjars it.path, filter: '!META-INF/versions/**' + } + + keepattributes '*' + dontpreverify + dontobfuscate + dontoptimize + verbose + + keep 'class *' +} +// DOCEND 01 + +defaultTasks "determinise" +determinise.finalizedBy metafix +metafix.finalizedBy checkDeterminism +assemble.dependsOn checkDeterminism + +def deterministicJar = metafix.outputs.files.singleFile +artifacts { + runtimeArtifacts file: deterministicJar, name: jarBaseName, type: 'jar', extension: 'jar', builtBy: metafix + publish file: deterministicJar, name: jarBaseName, type: 'jar', extension: 'jar', builtBy: metafix +} + +publish { + dependenciesFrom configurations.runtimeArtifacts + publishSources = false + publishJavadoc = false + name jarBaseName +} + +// Must be after publish {} so that the previous install task exists for overwriting. +task install(overwrite: true, dependsOn: 'publishToMavenLocal') diff --git a/core-deterministic/src/main/kotlin/net/corda/core/crypto/DelegatingSecureRandomService.kt b/core-deterministic/src/main/kotlin/net/corda/core/crypto/DelegatingSecureRandomService.kt new file mode 100644 index 0000000000..a3b100ae88 --- /dev/null +++ b/core-deterministic/src/main/kotlin/net/corda/core/crypto/DelegatingSecureRandomService.kt @@ -0,0 +1,28 @@ +/* + * R3 Proprietary and Confidential + * + * Copyright (c) 2018 R3 Limited. All rights reserved. + * + * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. + * + * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. + */ +package net.corda.core.crypto + +import java.security.Provider +import java.security.SecureRandomSpi + +@Suppress("unused") +class DelegatingSecureRandomService(provider: CordaSecurityProvider) + : Provider.Service(provider, "SecureRandom", "dummy-algorithm", UnsupportedSecureRandomSpi::javaClass.name, null, null) { + private val instance: SecureRandomSpi = UnsupportedSecureRandomSpi(algorithm) + override fun newInstance(param: Any?) = instance + + private class UnsupportedSecureRandomSpi(private val algorithm: String) : SecureRandomSpi() { + override fun engineSetSeed(seed: ByteArray) = unsupported() + override fun engineNextBytes(bytes: ByteArray) = unsupported() + override fun engineGenerateSeed(numBytes: Int) = unsupported() + + private fun unsupported(): Nothing = throw UnsupportedOperationException("$algorithm not supported") + } +} diff --git a/core-deterministic/src/main/kotlin/net/corda/core/crypto/SHA256DigestSupplier.kt b/core-deterministic/src/main/kotlin/net/corda/core/crypto/SHA256DigestSupplier.kt new file mode 100644 index 0000000000..0c1c3cfbca --- /dev/null +++ b/core-deterministic/src/main/kotlin/net/corda/core/crypto/SHA256DigestSupplier.kt @@ -0,0 +1,18 @@ +/* + * R3 Proprietary and Confidential + * + * Copyright (c) 2018 R3 Limited. All rights reserved. + * + * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. + * + * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. + */ +package net.corda.core.crypto + +import java.security.MessageDigest +import java.util.function.Supplier + +@Suppress("unused") +private class SHA256DigestSupplier : Supplier { + override fun get(): MessageDigest = MessageDigest.getInstance("SHA-256") +} diff --git a/core-deterministic/src/main/kotlin/net/corda/core/internal/ToggleField.kt b/core-deterministic/src/main/kotlin/net/corda/core/internal/ToggleField.kt new file mode 100644 index 0000000000..1912bd895e --- /dev/null +++ b/core-deterministic/src/main/kotlin/net/corda/core/internal/ToggleField.kt @@ -0,0 +1,62 @@ +package net.corda.core.internal + +import net.corda.core.KeepForDJVM +import net.corda.core.utilities.contextLogger +import org.slf4j.Logger +import kotlin.reflect.KProperty + +/** May go from null to non-null and vice-versa, and that's it. */ +abstract class ToggleField(val name: String) { + abstract fun get(): T? + fun set(value: T?) { + if (value != null) { + check(get() == null) { "$name already has a value." } + setImpl(value) + } else { + check(get() != null) { "$name is already null." } + clear() + } + } + + protected abstract fun setImpl(value: T) + protected abstract fun clear() + operator fun getValue(thisRef: Any?, property: KProperty<*>) = get() + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) = set(value) +} + +@KeepForDJVM +class SimpleToggleField(name: String, private val once: Boolean = false) : ToggleField(name) { + private var holder: T? = null // Force T? in API for safety. + override fun get() = holder + override fun setImpl(value: T) { holder = value } + override fun clear() { + check(!once) { "Value of $name cannot be changed." } + holder = null + } +} + +@KeepForDJVM +class ThreadLocalToggleField(name: String) : ToggleField(name) { + private var holder: T? = null // Force T? in API for safety. + override fun get() = holder + override fun setImpl(value: T) { holder = value } + override fun clear() { + holder = null + } +} + +@Suppress("UNUSED") +@KeepForDJVM +class InheritableThreadLocalToggleField(name: String, + private val log: Logger = staticLog, + private val isAGlobalThreadBeingCreated: (Array) -> Boolean) : ToggleField(name) { + private companion object { + private val staticLog = contextLogger() + } + private var holder: T? = null // Force T? in API for safety. + override fun get() = holder + override fun setImpl(value: T) { holder = value } + override fun clear() { + holder = null + } +} \ No newline at end of file diff --git a/core-deterministic/src/main/kotlin/net/corda/core/serialization/SerializationFactory.kt b/core-deterministic/src/main/kotlin/net/corda/core/serialization/SerializationFactory.kt new file mode 100644 index 0000000000..69ddb8887b --- /dev/null +++ b/core-deterministic/src/main/kotlin/net/corda/core/serialization/SerializationFactory.kt @@ -0,0 +1,95 @@ +package net.corda.core.serialization + +import net.corda.core.KeepForDJVM +import net.corda.core.serialization.internal.effectiveSerializationEnv +import net.corda.core.utilities.ByteSequence + +/** + * An abstraction for serializing and deserializing objects, with support for versioning of the wire format via + * a header / prefix in the bytes. + */ +@KeepForDJVM +abstract class SerializationFactory { + /** + * Deserialize the bytes in to an object, using the prefixed bytes to determine the format. + * + * @param byteSequence The bytes to deserialize, including a format header prefix. + * @param clazz The class or superclass or the object to be deserialized, or [Any] or [Object] if unknown. + * @param context A context that configures various parameters to deserialization. + */ + abstract fun deserialize(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): T + + /** + * Deserialize the bytes in to an object, using the prefixed bytes to determine the format. + * + * @param byteSequence The bytes to deserialize, including a format header prefix. + * @param clazz The class or superclass or the object to be deserialized, or [Any] or [Object] if unknown. + * @param context A context that configures various parameters to deserialization. + * @return deserialized object along with [SerializationContext] to identify encoding used. + */ + abstract fun deserializeWithCompatibleContext(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): ObjectWithCompatibleContext + + /** + * Serialize an object to bytes using the preferred serialization format version from the context. + * + * @param obj The object to be serialized. + * @param context A context that configures various parameters to serialization, including the serialization format version. + */ + abstract fun serialize(obj: T, context: SerializationContext): SerializedBytes + + /** + * If there is a need to nest serialization/deserialization with a modified context during serialization or deserialization, + * this will return the current context used to start serialization/deserialization. + */ + val currentContext: SerializationContext? get() = _currentContext + + /** + * A context to use as a default if you do not require a specially configured context. It will be the current context + * if the use is somehow nested (see [currentContext]). + */ + val defaultContext: SerializationContext get() = currentContext ?: effectiveSerializationEnv.p2pContext + + private var _currentContext: SerializationContext? = null + + /** + * Change the current context inside the block to that supplied. + */ + fun withCurrentContext(context: SerializationContext?, block: () -> T): T { + val priorContext = _currentContext + if (context != null) _currentContext = context + try { + return block() + } finally { + if (context != null) _currentContext = priorContext + } + } + + /** + * Allow subclasses to temporarily mark themselves as the current factory for the current thread during serialization/deserialization. + * Will restore the prior context on exiting the block. + */ + fun asCurrent(block: SerializationFactory.() -> T): T { + val priorContext = _currentFactory + _currentFactory= this + try { + return this.block() + } finally { + _currentFactory = priorContext + } + } + + companion object { + private var _currentFactory: SerializationFactory? = null + + /** + * A default factory for serialization/deserialization, taking into account the [currentFactory] if set. + */ + val defaultFactory: SerializationFactory get() = currentFactory ?: effectiveSerializationEnv.serializationFactory + + /** + * If there is a need to nest serialization/deserialization with a modified context during serialization or deserialization, + * this will return the current factory used to start serialization/deserialization. + */ + val currentFactory: SerializationFactory? get() = _currentFactory + } +} diff --git a/core-deterministic/src/main/kotlin/net/corda/core/utilities/SgxSupport.kt b/core-deterministic/src/main/kotlin/net/corda/core/utilities/SgxSupport.kt new file mode 100644 index 0000000000..ec1fb73ef8 --- /dev/null +++ b/core-deterministic/src/main/kotlin/net/corda/core/utilities/SgxSupport.kt @@ -0,0 +1,18 @@ +/* + * R3 Proprietary and Confidential + * + * Copyright (c) 2018 R3 Limited. All rights reserved. + * + * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. + * + * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. + */ +package net.corda.core.utilities + +import net.corda.core.KeepForDJVM + +@KeepForDJVM +object SgxSupport { + @JvmStatic + val isInsideEnclave: Boolean = true +} diff --git a/core-deterministic/testing/build.gradle b/core-deterministic/testing/build.gradle new file mode 100644 index 0000000000..89f06e350c --- /dev/null +++ b/core-deterministic/testing/build.gradle @@ -0,0 +1,25 @@ +/* + * R3 Proprietary and Confidential + * + * Copyright (c) 2018 R3 Limited. All rights reserved. + * + * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. + * + * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. + */ +apply plugin: 'kotlin' + +dependencies { + testCompile project(path: ':core-deterministic', configuration: 'runtimeArtifacts') + testCompile project(path: ':serialization-deterministic', configuration: 'runtimeArtifacts') + testCompile project(path: ':core-deterministic:testing:data', configuration: 'testData') + testCompile project(':core-deterministic:testing:common') + testCompile(project(':finance')) { + transitive = false + } + + testCompile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" + testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" + testCompile "org.assertj:assertj-core:$assertj_version" + testCompile "junit:junit:$junit_version" +} diff --git a/core-deterministic/testing/common/build.gradle b/core-deterministic/testing/common/build.gradle new file mode 100644 index 0000000000..0d8ee674c5 --- /dev/null +++ b/core-deterministic/testing/common/build.gradle @@ -0,0 +1,33 @@ +/* + * R3 Proprietary and Confidential + * + * Copyright (c) 2018 R3 Limited. All rights reserved. + * + * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. + * + * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. + */ +apply plugin: 'kotlin' + +evaluationDependsOn(':jdk8u-deterministic') + +def jdkTask = project(':jdk8u-deterministic').assemble +def deterministic_jdk_home = project(':jdk8u-deterministic').jdk_home + +dependencies { + compileOnly project(path: ':core-deterministic', configuration: 'runtimeArtifacts') + compileOnly project(path: ':serialization-deterministic', configuration: 'runtimeArtifacts') + compileOnly "junit:junit:$junit_version" +} + +tasks.withType(AbstractCompile) { + dependsOn jdkTask +} + +tasks.withType(JavaCompile) { + options.compilerArgs << '-bootclasspath' << "$deterministic_jdk_home/jre/lib/rt.jar".toString() +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) { + kotlinOptions.jdkHome = deterministic_jdk_home +} diff --git a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/LocalSerializationRule.kt b/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/LocalSerializationRule.kt new file mode 100644 index 0000000000..184f7f72d9 --- /dev/null +++ b/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/LocalSerializationRule.kt @@ -0,0 +1,85 @@ +package net.corda.deterministic.common + +import net.corda.core.serialization.ClassWhitelist +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.SerializationContext.UseCase.* +import net.corda.core.serialization.SerializationCustomSerializer +import net.corda.core.serialization.internal.SerializationEnvironmentImpl +import net.corda.core.serialization.internal._contextSerializationEnv +import net.corda.serialization.internal.* +import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme +import net.corda.serialization.internal.amqp.SerializerFactory +import net.corda.serialization.internal.amqp.amqpMagic +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import kotlin.reflect.KClass +import kotlin.reflect.jvm.jvmName + +class LocalSerializationRule(private val label: String) : TestRule { + constructor(klass: KClass<*>) : this(klass.jvmName) + + private companion object { + private val AMQP_P2P_CONTEXT = SerializationContextImpl( + amqpMagic, + LocalSerializationRule::class.java.classLoader, + GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()), + emptyMap(), + true, + P2P, + null + ) + } + + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + init() + try { + base.evaluate() + } finally { + clear() + } + } + } + } + + fun reset() { + clear() + init() + } + + private fun init() { + _contextSerializationEnv.set(createTestSerializationEnv()) + } + + private fun clear() { + _contextSerializationEnv.set(null) + } + + private fun createTestSerializationEnv(): SerializationEnvironmentImpl { + val factory = SerializationFactoryImpl(mutableMapOf()).apply { + registerScheme(AMQPSerializationScheme(emptySet(), mutableMapOf())) + } + return object : SerializationEnvironmentImpl(factory, AMQP_P2P_CONTEXT) { + override fun toString() = "testSerializationEnv($label)" + } + } + + private class AMQPSerializationScheme( + cordappCustomSerializers: Set>, + serializerFactoriesForContexts: MutableMap, SerializerFactory> + ) : AbstractAMQPSerializationScheme(cordappCustomSerializers, serializerFactoriesForContexts) { + override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory { + throw UnsupportedOperationException() + } + + override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory { + throw UnsupportedOperationException() + } + + override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean { + return canDeserializeVersion(magic) && target == P2P + } + } +} \ No newline at end of file diff --git a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/MockContractAttachment.kt b/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/MockContractAttachment.kt new file mode 100644 index 0000000000..1526825683 --- /dev/null +++ b/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/MockContractAttachment.kt @@ -0,0 +1,15 @@ +package net.corda.deterministic.common + +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.ContractClassName +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.Party +import net.corda.core.serialization.CordaSerializable +import java.io.ByteArrayInputStream +import java.io.InputStream + +@CordaSerializable +class MockContractAttachment(override val id: SecureHash = SecureHash.zeroHash, val contract: ContractClassName, override val signers: List = ArrayList()) : Attachment { + override fun open(): InputStream = ByteArrayInputStream(id.bytes) + override val size = id.size +} diff --git a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/SampleData.kt b/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/SampleData.kt new file mode 100644 index 0000000000..025fa148fa --- /dev/null +++ b/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/SampleData.kt @@ -0,0 +1,6 @@ +@file:JvmName("SampleData") +package net.corda.deterministic.common + +import net.corda.core.contracts.TypeOnlyCommandData + +object SampleCommandData : TypeOnlyCommandData() diff --git a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/TransactionVerificationRequest.kt b/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/TransactionVerificationRequest.kt new file mode 100644 index 0000000000..fb3743688d --- /dev/null +++ b/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/TransactionVerificationRequest.kt @@ -0,0 +1,32 @@ +package net.corda.deterministic.common + +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.ContractAttachment +import net.corda.core.contracts.ContractClassName +import net.corda.core.internal.TEST_UPLOADER +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.deserialize +import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.WireTransaction + +@Suppress("MemberVisibilityCanBePrivate") +@CordaSerializable +class TransactionVerificationRequest(val wtxToVerify: SerializedBytes, + val dependencies: Array>, + val attachments: Array) { + fun toLedgerTransaction(): LedgerTransaction { + val deps = dependencies.map { it.deserialize() }.associateBy(WireTransaction::id) + val attachments = attachments.map { it.deserialize() } + val attachmentMap = attachments.mapNotNull { it as? MockContractAttachment } + .associateBy(Attachment::id, { ContractAttachment(it, it.contract, uploader=TEST_UPLOADER) }) + val contractAttachmentMap = emptyMap() + @Suppress("DEPRECATION") + return wtxToVerify.deserialize().toLedgerTransaction( + resolveIdentity = { null }, + resolveAttachment = { attachmentMap[it] }, + resolveStateRef = { deps[it.txhash]?.outputs?.get(it.index) }, + resolveContractAttachment = { contractAttachmentMap[it.contract]?.id } + ) + } +} diff --git a/core-deterministic/testing/data/build.gradle b/core-deterministic/testing/data/build.gradle new file mode 100644 index 0000000000..7f4b4d9f91 --- /dev/null +++ b/core-deterministic/testing/data/build.gradle @@ -0,0 +1,38 @@ +/* + * R3 Proprietary and Confidential + * + * Copyright (c) 2018 R3 Limited. All rights reserved. + * + * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. + * + * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. + */ +apply plugin: 'kotlin' + +configurations { + testData +} + +dependencies { + testCompile project(':core') + testCompile project(':finance') + testCompile project(':node-driver') + testCompile project(':core-deterministic:testing:common') + + testCompile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + testCompile "org.jetbrains.kotlin:kotlin-reflect" + testCompile "junit:junit:$junit_version" +} + +jar.enabled = false + +test { + filter { + // Running this class is the whole point, so include it explicitly. + includeTestsMatching "net.corda.deterministic.data.GenerateData" + } +} + +artifacts { + testData file: file("$buildDir/test-data.jar"), type: 'jar', builtBy: test +} diff --git a/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/GenerateData.kt b/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/GenerateData.kt new file mode 100644 index 0000000000..0304661183 --- /dev/null +++ b/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/GenerateData.kt @@ -0,0 +1,92 @@ +package net.corda.deterministic.data + +import net.corda.core.serialization.deserialize +import net.corda.deterministic.common.LocalSerializationRule +import net.corda.deterministic.common.TransactionVerificationRequest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.io.FileNotFoundException +import java.net.URLClassLoader +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.attribute.FileTime +import java.util.* +import java.util.Calendar.* +import java.util.jar.JarOutputStream +import java.util.zip.Deflater.NO_COMPRESSION +import java.util.zip.ZipEntry +import java.util.zip.ZipEntry.* +import kotlin.reflect.jvm.jvmName + +/** + * Use the JUnit framework to generate a JAR of test data. + */ +class GenerateData { + companion object { + private val CONSTANT_TIME: FileTime = FileTime.fromMillis( + GregorianCalendar(1980, FEBRUARY, 1).apply { timeZone = TimeZone.getTimeZone("UTC") }.timeInMillis + ) + private const val KEYSTORE_ALIAS = "tx" + private val KEYSTORE_PASSWORD = "deterministic".toCharArray() + private val TEST_DATA: Path = Paths.get("build", "test-data.jar") + + private fun compressed(name: String) = ZipEntry(name).apply { + lastModifiedTime = CONSTANT_TIME + method = DEFLATED + } + + private fun directory(name: String) = ZipEntry(name).apply { + lastModifiedTime = CONSTANT_TIME + method = STORED + compressedSize = 0 + size = 0 + crc = 0 + } + } + + @Rule + @JvmField + val testSerialization = LocalSerializationRule(GenerateData::class.jvmName) + + @Before + fun createTransactions() { + JarOutputStream(Files.newOutputStream(TEST_DATA)).use { jar -> + jar.setComment("Test data for Deterministic Corda") + jar.setLevel(NO_COMPRESSION) + + // Serialised transactions for the Enclavelet + jar.putNextEntry(directory("txverify")) + jar.putNextEntry(compressed("txverify/tx-success.bin")) + TransactionGenerator.writeSuccess(jar) + jar.putNextEntry(compressed("txverify/tx-failure.bin")) + TransactionGenerator.writeFailure(jar) + + // KeyStore containing an EC private key. + jar.putNextEntry(directory("keystore")) + jar.putNextEntry(compressed("keystore/txsignature.pfx")) + KeyStoreGenerator.writeKeyStore(jar, KEYSTORE_ALIAS, KEYSTORE_PASSWORD) + } + testSerialization.reset() + } + + @Test + fun verifyTransactions() { + URLClassLoader(arrayOf(TEST_DATA.toUri().toURL())).use { cl -> + cl.loadResource("txverify/tx-success.bin") + .deserialize() + .toLedgerTransaction() + .verify() + + cl.loadResource("txverify/tx-failure.bin") + .deserialize() + .toLedgerTransaction() + } + } + + private fun ClassLoader.loadResource(resourceName: String): ByteArray { + return getResourceAsStream(resourceName)?.use { it.readBytes() } + ?: throw FileNotFoundException(resourceName) + } +} diff --git a/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/KeyStoreGenerator.kt b/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/KeyStoreGenerator.kt new file mode 100644 index 0000000000..f14f933f10 --- /dev/null +++ b/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/KeyStoreGenerator.kt @@ -0,0 +1,51 @@ +package net.corda.deterministic.data + +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import java.io.OutputStream +import java.math.BigInteger +import java.math.BigInteger.TEN +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.spec.ECGenParameterSpec +import java.util.* +import java.util.Calendar.* + +object KeyStoreGenerator { + private val keyPairGenerator: KeyPairGenerator = KeyPairGenerator.getInstance("EC").apply { + initialize(ECGenParameterSpec("secp256k1")) + } + + fun writeKeyStore(output: OutputStream, alias: String, password: CharArray) { + val keyPair = keyPairGenerator.generateKeyPair() + val signer = JcaContentSignerBuilder("SHA256WithECDSA").build(keyPair.private) + val dname = X500Name("CN=Enclavelet") + val startDate = Calendar.getInstance().let { cal -> + cal.time = Date() + cal.add(HOUR, -1) + cal.time + } + val endDate = Calendar.getInstance().let { cal -> + cal.time = startDate + cal.add(YEAR, 1) + cal.time + } + val certificate = JcaX509v3CertificateBuilder( + dname, + TEN, + startDate, + endDate, + dname, + keyPair.public + ).build(signer) + val x509 = JcaX509CertificateConverter().getCertificate(certificate) + + KeyStore.getInstance("PKCS12").apply { + load(null, password) + setKeyEntry(alias, keyPair.private, password, arrayOf(x509)) + store(output, password) + } + } +} \ No newline at end of file diff --git a/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/TransactionGenerator.kt b/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/TransactionGenerator.kt new file mode 100644 index 0000000000..a6b704077e --- /dev/null +++ b/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/TransactionGenerator.kt @@ -0,0 +1,112 @@ +package net.corda.deterministic.data + +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.crypto.entropyToKeyPair +import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party +import net.corda.core.serialization.serialize +import net.corda.deterministic.common.MockContractAttachment +import net.corda.deterministic.common.SampleCommandData +import net.corda.deterministic.common.TransactionVerificationRequest +import net.corda.finance.POUNDS +import net.corda.finance.`issued by` +import net.corda.finance.contracts.asset.Cash.* +import net.corda.finance.contracts.asset.Cash.Commands.* +import net.corda.finance.contracts.asset.Cash.Companion.PROGRAM_ID +import net.corda.node.services.api.IdentityServiceInternal +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.core.TestIdentity +import net.corda.testing.core.getTestPartyAndCertificate +import net.corda.testing.internal.rigorousMock +import net.corda.testing.node.MockServices +import net.corda.testing.node.ledger +import java.io.OutputStream +import java.math.BigInteger +import java.security.KeyPair +import java.security.PublicKey + +object TransactionGenerator { + private val DUMMY_NOTARY: Party = TestIdentity(DUMMY_NOTARY_NAME, 20).party + + private val DUMMY_CASH_ISSUER_KEY: KeyPair = entropyToKeyPair(BigInteger.valueOf(10)) + private val DUMMY_CASH_ISSUER_IDENTITY = getTestPartyAndCertificate(Party(CordaX500Name("Snake Oil Issuer", "London", "GB"), DUMMY_CASH_ISSUER_KEY.public)) + private val DUMMY_CASH_ISSUER = DUMMY_CASH_ISSUER_IDENTITY.party.ref(1) + + private val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) + private val MEGA_CORP: Party = megaCorp.party + private val MEGA_CORP_PUBKEY: PublicKey = megaCorp.keyPair.public + private val MINI_CORP_PUBKEY: PublicKey = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).keyPair.public + + private val ledgerServices = MockServices(emptyList(), MEGA_CORP.name, rigorousMock().also { + doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) + doReturn(DUMMY_CASH_ISSUER.party).whenever(it).partyFromKey(DUMMY_CASH_ISSUER_KEY.public) + }) + + fun writeSuccess(output: OutputStream) { + ledgerServices.ledger(DUMMY_NOTARY) { + // Issue a couple of cash states and spend them. + val wtx1 = transaction { + attachments(PROGRAM_ID) + output(PROGRAM_ID, "c1", State(1000.POUNDS `issued by` DUMMY_CASH_ISSUER, AnonymousParty(MEGA_CORP_PUBKEY))) + command(DUMMY_CASH_ISSUER.party.owningKey, Issue()) + verifies() + } + val wtx2 = transaction { + attachments(PROGRAM_ID) + output(PROGRAM_ID, "c2", State(2000.POUNDS `issued by` DUMMY_CASH_ISSUER, AnonymousParty(MEGA_CORP_PUBKEY))) + command(DUMMY_CASH_ISSUER.party.owningKey, Issue()) + verifies() + } + val wtx3 = transaction { + attachments(PROGRAM_ID) + input("c1") + input("c2") + output(PROGRAM_ID, "c3", State(3000.POUNDS `issued by` DUMMY_CASH_ISSUER, AnonymousParty(MINI_CORP_PUBKEY))) + command(MEGA_CORP_PUBKEY, Move()) + verifies() + } + val contractAttachment = MockContractAttachment(interpreter.services.cordappProvider.getContractAttachmentID(PROGRAM_ID)!!, PROGRAM_ID) + TransactionVerificationRequest( + wtx3.serialize(), + arrayOf(wtx1.serialize(), wtx2.serialize()), + arrayOf(contractAttachment.serialize().bytes)) + .serialize() + .writeTo(output) + } + } + + fun writeFailure(output: OutputStream) { + ledgerServices.ledger(DUMMY_NOTARY) { + // Issue a couple of cash states and spend them. + val wtx1 = transaction { + attachments(PROGRAM_ID) + output(PROGRAM_ID, "c1", State(1000.POUNDS `issued by` DUMMY_CASH_ISSUER, AnonymousParty(MEGA_CORP_PUBKEY))) + command(DUMMY_CASH_ISSUER.party.owningKey, Issue()) + verifies() + } + val wtx2 = transaction { + attachments(PROGRAM_ID) + output(PROGRAM_ID, "c2", State(2000.POUNDS `issued by` DUMMY_CASH_ISSUER, AnonymousParty(MEGA_CORP_PUBKEY))) + command(DUMMY_CASH_ISSUER.party.owningKey, Issue()) + verifies() + } + val wtx3 = transaction { + attachments(PROGRAM_ID) + input("c1") + input("c2") + command(DUMMY_CASH_ISSUER.party.owningKey, SampleCommandData) + output(PROGRAM_ID, "c3", State(3000.POUNDS `issued by` DUMMY_CASH_ISSUER, AnonymousParty(MINI_CORP_PUBKEY))) + failsWith("Required ${Move::class.java.canonicalName} command") + } + val contractAttachment = MockContractAttachment(interpreter.services.cordappProvider.getContractAttachmentID(PROGRAM_ID)!!, PROGRAM_ID) + TransactionVerificationRequest( + wtx3.serialize(), + arrayOf(wtx1.serialize(), wtx2.serialize()), + arrayOf(contractAttachment.serialize().bytes)) + .serialize() + .writeTo(output) + } + } +} \ No newline at end of file diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/CheatingSecurityProvider.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/CheatingSecurityProvider.kt new file mode 100644 index 0000000000..048c0cae68 --- /dev/null +++ b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/CheatingSecurityProvider.kt @@ -0,0 +1,43 @@ +package net.corda.deterministic + +import org.junit.Assert.* +import java.security.Provider +import java.security.SecureRandom +import java.security.SecureRandomSpi +import java.security.Security + +/** + * Temporarily restore Sun's [SecureRandom] provider. + * This is ONLY for allowing us to generate test data, e.g. signatures. + */ +class CheatingSecurityProvider : Provider(NAME, 1.8, "$NAME security provider"), AutoCloseable { + private companion object { + private const val NAME = "Cheat!" + } + + init { + putService(CheatingSecureRandomService(this)) + assertEquals(1, Security.insertProviderAt(this, 1)) + } + + override fun close() { + Security.removeProvider(NAME) + } + + private class SunSecureRandom : SecureRandom(sun.security.provider.SecureRandom(), null) + + private class CheatingSecureRandomService(provider: Provider) + : Provider.Service(provider, "SecureRandom", "CheatingPRNG", CheatingSecureRandomSpi::javaClass.name, null, null) { + + private val instance: SecureRandomSpi = CheatingSecureRandomSpi() + override fun newInstance(constructorParameter: Any?) = instance + } + + private class CheatingSecureRandomSpi : SecureRandomSpi() { + private val secureRandom: SecureRandom = SunSecureRandom() + + override fun engineSetSeed(seed: ByteArray) = secureRandom.setSeed(seed) + override fun engineNextBytes(bytes: ByteArray) = secureRandom.nextBytes(bytes) + override fun engineGenerateSeed(numBytes: Int): ByteArray = secureRandom.generateSeed(numBytes) + } +} \ No newline at end of file diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/CordaExceptionTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/CordaExceptionTest.kt new file mode 100644 index 0000000000..2a7351ae08 --- /dev/null +++ b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/CordaExceptionTest.kt @@ -0,0 +1,70 @@ +package net.corda.deterministic + +import net.corda.core.CordaException +import net.corda.core.contracts.AttachmentResolutionException +import net.corda.core.contracts.TransactionResolutionException +import net.corda.core.contracts.TransactionVerificationException.* +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party +import org.junit.Assert.* +import org.junit.Test +import java.security.PublicKey +import kotlin.test.assertFailsWith + +class CordaExceptionTest { + companion object { + const val CONTRACT_CLASS = "com.r3.corda.contracts.TestContract" + val TEST_HASH = SecureHash.zeroHash + val TX_ID = SecureHash.allOnesHash + + val ALICE_NAME = CordaX500Name("Alice Corp", "Madrid", "ES") + val ALICE_KEY: PublicKey = object : PublicKey { + override fun getAlgorithm(): String = "TEST-256" + override fun getFormat(): String = "" + override fun getEncoded() = byteArrayOf() + } + val ALICE = Party(ALICE_NAME, ALICE_KEY) + + val BOB_NAME = CordaX500Name("Bob Plc", "Rome", "IT") + val BOB_KEY: PublicKey = object : PublicKey { + override fun getAlgorithm(): String = "TEST-512" + override fun getFormat(): String = "" + override fun getEncoded() = byteArrayOf() + } + val BOB = Party(BOB_NAME, BOB_KEY) + } + + @Test + fun testCordaException() { + val ex = assertFailsWith { throw CordaException("BAD THING") } + assertEquals("BAD THING", ex.message) + } + + @Test + fun testAttachmentResolutionException() { + val ex = assertFailsWith { throw AttachmentResolutionException(TEST_HASH) } + assertEquals(TEST_HASH, ex.hash) + } + + @Test + fun testTransactionResolutionException() { + val ex = assertFailsWith { throw TransactionResolutionException(TEST_HASH) } + assertEquals(TEST_HASH, ex.hash) + } + + @Test + fun testConflictingAttachmentsRejection() { + val ex = assertFailsWith { throw ConflictingAttachmentsRejection(TX_ID, CONTRACT_CLASS) } + assertEquals(TX_ID, ex.txId) + assertEquals(CONTRACT_CLASS, ex.contractClass) + } + + @Test + fun testNotaryChangeInWrongTransactionType() { + val ex = assertFailsWith { throw NotaryChangeInWrongTransactionType(TX_ID, ALICE, BOB) } + assertEquals(TX_ID, ex.txId) + assertEquals(ALICE, ex.txNotary) + assertEquals(BOB, ex.outputNotary) + } +} \ No newline at end of file diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/KeyStoreProvider.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/KeyStoreProvider.kt new file mode 100644 index 0000000000..20929a557a --- /dev/null +++ b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/KeyStoreProvider.kt @@ -0,0 +1,44 @@ +package net.corda.deterministic + +import org.junit.AssumptionViolatedException +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import java.security.KeyPair +import java.security.KeyStore +import java.security.PrivateKey +import java.security.cert.TrustAnchor +import java.security.cert.X509Certificate + +class KeyStoreProvider(private val storeName: String, private val storePassword: String) : TestRule { + private lateinit var keyStore: KeyStore + + private fun loadKeyStoreResource(resourceName: String, password: CharArray, type: String = "PKCS12"): KeyStore { + return KeyStore.getInstance(type).apply { + // Skip these tests if we cannot load the keystore. + val keyStream = KeyStoreProvider::class.java.classLoader.getResourceAsStream(resourceName) + ?: throw AssumptionViolatedException("KeyStore $resourceName not found") + keyStream.use { input -> + load(input, password) + } + } + } + + override fun apply(statement: Statement, description: Description?): Statement { + return object : Statement() { + override fun evaluate() { + keyStore = loadKeyStoreResource(storeName, storePassword.toCharArray()) + statement.evaluate() + } + } + } + + fun getKeyPair(alias: String): KeyPair { + val privateKey = keyStore.getKey(alias, storePassword.toCharArray()) as PrivateKey + return KeyPair(keyStore.getCertificate(alias).publicKey, privateKey) + } + + @Suppress("UNUSED") + fun trustAnchorsFor(vararg aliases: String): Set + = aliases.map { alias -> TrustAnchor(keyStore.getCertificate(alias) as X509Certificate, null) }.toSet() +} diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/Utilities.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/Utilities.kt new file mode 100644 index 0000000000..bb7290206e --- /dev/null +++ b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/Utilities.kt @@ -0,0 +1,14 @@ +package net.corda.deterministic + +import java.io.ByteArrayOutputStream +import java.io.IOException + +private val classLoader: ClassLoader = object {}.javaClass.classLoader + +@Throws(IOException::class) +fun bytesOfResource(resourceName: String): ByteArray { + return ByteArrayOutputStream().let { baos -> + classLoader.getResourceAsStream(resourceName).copyTo(baos) + baos.toByteArray() + } +} diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/AttachmentTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/AttachmentTest.kt new file mode 100644 index 0000000000..21ad6a3e45 --- /dev/null +++ b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/AttachmentTest.kt @@ -0,0 +1,77 @@ +package net.corda.deterministic.contracts + +import net.corda.core.contracts.Attachment +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.security.PublicKey +import java.util.jar.JarOutputStream +import java.util.zip.Deflater.* +import java.util.zip.ZipEntry + +class AttachmentTest { + private companion object { + private val data = byteArrayOf(0x73, 0x71, 0x18, 0x5F, 0x3A, 0x47, -0x22, 0x38) + private val jarData: ByteArray = ByteArrayOutputStream().let { baos -> + JarOutputStream(baos).use { jar -> + jar.setLevel(BEST_COMPRESSION) + jar.putNextEntry(ZipEntry("data.bin").apply { method = DEFLATED }) + data.inputStream().copyTo(jar) + } + baos.toByteArray() + } + + private val ALICE_NAME = CordaX500Name("Alice Corp", "Madrid", "ES") + private val ALICE_KEY: PublicKey = object : PublicKey { + override fun getAlgorithm(): String = "TEST-256" + override fun getFormat(): String = "" + override fun getEncoded() = byteArrayOf() + } + private val ALICE = Party(ALICE_NAME, ALICE_KEY) + } + + private lateinit var attachment: Attachment + + @Before + fun setup() { + attachment = object : Attachment { + override val id: SecureHash + get() = SecureHash.allOnesHash + override val signers: List + get() = listOf(ALICE) + override val size: Int + get() = jarData.size + + override fun open(): InputStream { + return jarData.inputStream() + } + } + } + + @Test + fun testAttachmentJar() { + attachment.openAsJAR().use { jar -> + val entry = jar.nextJarEntry ?: return@use + assertEquals("data.bin", entry.name) + val entryData = ByteArrayOutputStream().use { + jar.copyTo(it) + it.toByteArray() + } + assertArrayEquals(data, entryData) + } + } + + @Test + fun testExtractFromAttachment() { + val resultData = ByteArrayOutputStream().use { + attachment.extractFile("data.bin", it) + it.toByteArray() + } + assertArrayEquals(data, resultData) + } +} \ No newline at end of file diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/PrivacySaltTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/PrivacySaltTest.kt new file mode 100644 index 0000000000..5b66cf520e --- /dev/null +++ b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/PrivacySaltTest.kt @@ -0,0 +1,34 @@ +package net.corda.deterministic.contracts + +import net.corda.core.contracts.PrivacySalt +import org.junit.Test +import kotlin.test.* + +class PrivacySaltTest { + private companion object { + private const val SALT_SIZE = 32 + } + + @Test + fun testValidSalt() { + PrivacySalt(ByteArray(SALT_SIZE, { 0x14 })) + } + + @Test + fun testInvalidSaltWithAllZeros() { + val ex = assertFailsWith { PrivacySalt(ByteArray(SALT_SIZE)) } + assertEquals("Privacy salt should not be all zeros.", ex.message) + } + + @Test + fun testTooShortPrivacySalt() { + val ex = assertFailsWith { PrivacySalt(ByteArray(SALT_SIZE - 1, { 0x7f })) } + assertEquals("Privacy salt should be 32 bytes.", ex.message) + } + + @Test + fun testTooLongPrivacySalt() { + val ex = assertFailsWith { PrivacySalt(ByteArray(SALT_SIZE + 1, { 0x7f })) } + assertEquals("Privacy salt should be 32 bytes.", ex.message) + } +} \ No newline at end of file diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/MerkleTreeTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/MerkleTreeTest.kt new file mode 100644 index 0000000000..432a89280d --- /dev/null +++ b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/MerkleTreeTest.kt @@ -0,0 +1,14 @@ +package net.corda.deterministic.crypto + +import net.corda.core.crypto.MerkleTree +import net.corda.core.crypto.SecureHash +import org.junit.Assert.assertEquals +import org.junit.Test + +class MerkleTreeTest { + @Test + fun testCreate() { + val merkle = MerkleTree.getMerkleTree(listOf(SecureHash.allOnesHash, SecureHash.zeroHash)) + assertEquals(SecureHash.parse("A5DE9B714ACCD8AFAAABF1CBD6E1014C9D07FF95C2AE154D91EC68485B31E7B5"), merkle.hash) + } +} \ No newline at end of file diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/SecureHashTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/SecureHashTest.kt new file mode 100644 index 0000000000..a9a069f589 --- /dev/null +++ b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/SecureHashTest.kt @@ -0,0 +1,42 @@ +package net.corda.deterministic.crypto + +import net.corda.core.crypto.SecureHash +import org.bouncycastle.util.encoders.Hex +import org.junit.Assert.* +import org.junit.Test +import java.security.MessageDigest + +class SecureHashTest { + @Test + fun testSHA256() { + val hash = SecureHash.sha256(byteArrayOf(0x64, -0x13, 0x42, 0x3a)) + assertEquals(SecureHash.parse("6D1687C143DF792A011A1E80670A4E4E0C25D0D87A39514409B1ABFC2043581F"), hash) + assertEquals("6D1687C143DF792A011A1E80670A4E4E0C25D0D87A39514409B1ABFC2043581F", hash.toString()) + } + + @Test + fun testPrefix() { + val data = byteArrayOf(0x7d, 0x03, -0x21, 0x32, 0x56, 0x47) + val digest = data.digestFor("SHA-256") + val prefix = SecureHash.sha256(data).prefixChars(8) + assertEquals(Hex.toHexString(digest).substring(0, 8).toUpperCase(), prefix) + } + + @Test + fun testConcat() { + val hash1 = SecureHash.sha256(byteArrayOf(0x7d, 0x03, -0x21, 0x32, 0x56, 0x47)) + val hash2 = SecureHash.sha256(byteArrayOf(0x63, 0x01, 0x7f, -0x29, 0x1e, 0x3c)) + val combined = hash1.hashConcat(hash2) + assertArrayEquals((hash1.bytes + hash2.bytes).digestFor("SHA-256"), combined.bytes) + } + + @Test + fun testConstants() { + assertArrayEquals(SecureHash.zeroHash.bytes, ByteArray(32)) + assertArrayEquals(SecureHash.allOnesHash.bytes, ByteArray(32, { 0xFF.toByte() })) + } +} + +private fun ByteArray.digestFor(algorithm: String): ByteArray { + return MessageDigest.getInstance(algorithm).digest(this) +} \ No newline at end of file diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/SecureRandomTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/SecureRandomTest.kt new file mode 100644 index 0000000000..bd5ae65a49 --- /dev/null +++ b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/SecureRandomTest.kt @@ -0,0 +1,22 @@ +package net.corda.deterministic.crypto + +import net.corda.core.crypto.CordaSecurityProvider +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import java.security.NoSuchAlgorithmException +import java.security.SecureRandom +import kotlin.test.assertFailsWith + +class SecureRandomTest { + private companion object { + init { + CordaSecurityProvider() + } + } + + @Test + fun testNoCordaPRNG() { + val error = assertFailsWith { SecureRandom.getInstance("CordaPRNG") } + assertThat(error).hasMessage("CordaPRNG SecureRandom not available") + } +} \ No newline at end of file diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt new file mode 100644 index 0000000000..5935c13b3c --- /dev/null +++ b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt @@ -0,0 +1,143 @@ +package net.corda.deterministic.crypto + +import net.corda.core.crypto.* +import net.corda.deterministic.KeyStoreProvider +import net.corda.deterministic.CheatingSecurityProvider +import net.corda.deterministic.common.LocalSerializationRule +import org.junit.* +import org.junit.rules.RuleChain +import java.security.* +import kotlin.test.* + +class TransactionSignatureTest { + companion object { + private const val KEYSTORE_PASSWORD = "deterministic" + private val testBytes = "12345678901234567890123456789012".toByteArray() + + private val keyStoreProvider = KeyStoreProvider("keystore/txsignature.pfx", KEYSTORE_PASSWORD) + private lateinit var keyPair: KeyPair + + @ClassRule + @JvmField + val rules: RuleChain = RuleChain.outerRule(LocalSerializationRule(TransactionSignatureTest::class)) + .around(keyStoreProvider) + + @BeforeClass + @JvmStatic + fun setupClass() { + keyPair = keyStoreProvider.getKeyPair("tx") + } + } + + /** Valid sign and verify. */ + @Test + fun `Signature metadata full sign and verify`() { + // Create a SignableData object. + val signableData = SignableData(testBytes.sha256(), SignatureMetadata(1, Crypto.findSignatureScheme(keyPair.public).schemeNumberID)) + + // Sign the meta object. + val transactionSignature: TransactionSignature = CheatingSecurityProvider().use { + keyPair.sign(signableData) + } + + // Check auto-verification. + assertTrue(transactionSignature.verify(testBytes.sha256())) + + // Check manual verification. + assertTrue(Crypto.doVerify(testBytes.sha256(), transactionSignature)) + } + + /** Verification should fail; corrupted metadata - clearData (Merkle root) has changed. */ + @Test(expected = SignatureException::class) + fun `Signature metadata full failure clearData has changed`() { + val signableData = SignableData(testBytes.sha256(), SignatureMetadata(1, Crypto.findSignatureScheme(keyPair.public).schemeNumberID)) + val transactionSignature = CheatingSecurityProvider().use { + keyPair.sign(signableData) + } + Crypto.doVerify((testBytes + testBytes).sha256(), transactionSignature) + } + + @Test + fun `Verify multi-tx signature`() { + // Deterministically create 5 txIds. + val txIds: List = IntRange(0, 4).map { byteArrayOf(it.toByte()).sha256() } + // Multi-tx signature. + val txSignature = signMultipleTx(txIds, keyPair) + + // The hash of all txIds are used as leaves. + val merkleTree = MerkleTree.getMerkleTree(txIds.map { it.sha256() }) + + // We haven't added the partial tree yet. + assertNull(txSignature.partialMerkleTree) + // Because partial tree is still null, but we signed over a block of txs, verifying a single tx will fail. + assertFailsWith { Crypto.doVerify(txIds[3], txSignature) } + + // Create a partial tree for one tx. + val pmt = PartialMerkleTree.build(merkleTree, listOf(txIds[0].sha256())) + // Add the partial Merkle tree to the tx signature. + val txSignatureWithTree = TransactionSignature(txSignature.bytes, txSignature.by, txSignature.signatureMetadata, pmt) + + // Verify the corresponding txId with every possible way. + assertTrue(Crypto.doVerify(txIds[0], txSignatureWithTree)) + assertTrue(txSignatureWithTree.verify(txIds[0])) + assertTrue(Crypto.isValid(txIds[0], txSignatureWithTree)) + assertTrue(txSignatureWithTree.isValid(txIds[0])) + + // Verify the rest txs in the block, which are not included in the partial Merkle tree. + txIds.subList(1, txIds.size).forEach { + assertFailsWith { Crypto.doVerify(it, txSignatureWithTree) } + } + + // Test that the Merkle tree consists of hash(txId), not txId. + assertFailsWith { PartialMerkleTree.build(merkleTree, listOf(txIds[0])) } + + // What if we send the Full tree. This could be used if notaries didn't want to create a per tx partial tree. + // Create a partial tree for all txs, thus all leaves are included. + val pmtFull = PartialMerkleTree.build(merkleTree, txIds.map { it.sha256() }) + // Add the partial Merkle tree to the tx. + val txSignatureWithFullTree = TransactionSignature(txSignature.bytes, txSignature.by, txSignature.signatureMetadata, pmtFull) + + // All txs can be verified, as they are all included in the provided partial tree. + txIds.forEach { + assertTrue(Crypto.doVerify(it, txSignatureWithFullTree)) + } + } + + @Test + fun `Verify one-tx signature`() { + val txId = "aTransaction".toByteArray().sha256() + // One-tx signature. + val txSignature = try { + signOneTx(txId, keyPair) + } catch (e: Throwable) { + e.cause?.printStackTrace() + throw e + } + + // partialMerkleTree should be null. + assertNull(txSignature.partialMerkleTree) + // Verify the corresponding txId with every possible way. + assertTrue(Crypto.doVerify(txId, txSignature)) + assertTrue(txSignature.verify(txId)) + assertTrue(Crypto.isValid(txId, txSignature)) + assertTrue(txSignature.isValid(txId)) + + // We signed the txId itself, not its hash (because it was a signature over one tx only and no partial tree has been received). + assertFailsWith { Crypto.doVerify(txId.sha256(), txSignature) } + } + + // Returns a TransactionSignature over the Merkle root, but the partial tree is null. + private fun signMultipleTx(txIds: List, keyPair: KeyPair): TransactionSignature { + val merkleTreeRoot = MerkleTree.getMerkleTree(txIds.map { it.sha256() }).hash + return signOneTx(merkleTreeRoot, keyPair) + } + + // Returns a TransactionSignature over one SecureHash. + // Note that if one tx is to be signed, we don't create a Merkle tree and we directly sign over the txId. + private fun signOneTx(txId: SecureHash, keyPair: KeyPair): TransactionSignature { + val signableData = SignableData(txId, SignatureMetadata(3, Crypto.findSignatureScheme(keyPair.public).schemeNumberID)) + return CheatingSecurityProvider().use { + keyPair.sign(signableData) + } + } +} diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/transactions/TransactionWithSignaturesTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/transactions/TransactionWithSignaturesTest.kt new file mode 100644 index 0000000000..f1ec9630db --- /dev/null +++ b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/transactions/TransactionWithSignaturesTest.kt @@ -0,0 +1,30 @@ +package net.corda.deterministic.transactions + +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.TransactionSignature +import net.corda.core.transactions.TransactionWithSignatures +import org.junit.Test +import java.security.PublicKey + +class TransactionWithSignaturesTest { + @Test + fun txWithSigs() { + val tx = object : TransactionWithSignatures { + override val id: SecureHash + get() = SecureHash.zeroHash + override val requiredSigningKeys: Set + get() = emptySet() + override val sigs: List + get() = emptyList() + + override fun getKeyDescriptions(keys: Set): List { + return emptyList() + } + } + tx.verifyRequiredSignatures() + tx.checkSignaturesAreValid() + tx.getMissingSigners() + tx.verifySignaturesExcept() + tx.verifySignaturesExcept(emptySet()) + } +} \ No newline at end of file diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/EnclaveletTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/EnclaveletTest.kt new file mode 100644 index 0000000000..88125a4072 --- /dev/null +++ b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/EnclaveletTest.kt @@ -0,0 +1,52 @@ +@file:JvmName("Enclavelet") +package net.corda.deterministic.txverify + +import net.corda.core.serialization.deserialize +import net.corda.core.transactions.LedgerTransaction +import net.corda.deterministic.bytesOfResource +import net.corda.deterministic.common.LocalSerializationRule +import net.corda.deterministic.common.TransactionVerificationRequest +import net.corda.finance.contracts.asset.Cash.Commands.* +import org.assertj.core.api.Assertions.assertThat +import org.junit.ClassRule +import org.junit.Test +import kotlin.test.assertFailsWith + +class EnclaveletTest { + companion object { + @ClassRule + @JvmField + val serialization = LocalSerializationRule(EnclaveletTest::class) + } + + @Test + fun success() { + verifyInEnclave(bytesOfResource("txverify/tx-success.bin")) + } + + @Test + fun failure() { + val e = assertFailsWith { verifyInEnclave(bytesOfResource("txverify/tx-failure.bin")) } + assertThat(e).hasMessageContaining("Required ${Move::class.java.canonicalName} command") + } +} + +/** + * Returns either null to indicate success when the transactions are validated, or a string with the + * contents of the error. Invoked via JNI in response to an enclave RPC. The argument is a serialised + * [TransactionVerificationRequest]. + * + * Note that it is assumed the signatures were already checked outside the sandbox: the purpose of this code + * is simply to check the sensitive, app specific parts of a transaction. + * + * TODO: Transaction data is meant to be encrypted under an enclave-private key. + */ +@Throws(Exception::class) +private fun verifyInEnclave(reqBytes: ByteArray) { + deserialize(reqBytes).verify() +} + +private fun deserialize(reqBytes: ByteArray): LedgerTransaction { + return reqBytes.deserialize() + .toLedgerTransaction() +} diff --git a/core-deterministic/testing/src/test/resources/log4j2-test.xml b/core-deterministic/testing/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000000..4e309bf567 --- /dev/null +++ b/core-deterministic/testing/src/test/resources/log4j2-test.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/core/build.gradle b/core/build.gradle index fea398862e..4a98c2bedf 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -66,12 +66,6 @@ processSmokeTestResources { } } -buildscript { - repositories { - mavenCentral() - } -} - dependencies { testCompile "junit:junit:$junit_version" testCompile "commons-fileupload:commons-fileupload:$fileupload_version" @@ -140,7 +134,10 @@ task copyQuasarJar(type: Copy) { rename { filename -> "quasar.jar"} } -jar.finalizedBy(copyQuasarJar) +jar { + finalizedBy(copyQuasarJar) + baseName 'corda-core' +} configurations { testArtifacts.extendsFrom testRuntime @@ -171,10 +168,6 @@ artifacts { testArtifacts testJar } -jar { - baseName 'corda-core' -} - scanApi { excludeClasses = [ // Kotlin should probably have declared this class as "synthetic". diff --git a/core/src/main/java/net/corda/core/crypto/Base58.java b/core/src/main/java/net/corda/core/crypto/Base58.java index 23840eb3f3..e7953d1596 100644 --- a/core/src/main/java/net/corda/core/crypto/Base58.java +++ b/core/src/main/java/net/corda/core/crypto/Base58.java @@ -10,6 +10,7 @@ package net.corda.core.crypto; +import net.corda.core.KeepForDJVM; import java.math.*; import java.util.*; @@ -37,6 +38,7 @@ import java.util.*; * NB: This class originally comes from the Apache licensed bitcoinj library. The original author of this code is the * same as the original author of the R3 repository. */ +@KeepForDJVM public class Base58 { private static final char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray(); private static final char ENCODED_ZERO = ALPHABET[0]; diff --git a/core/src/main/java/net/corda/core/flows/IdentifiableException.java b/core/src/main/java/net/corda/core/flows/IdentifiableException.java index e6d7c78f91..613a17bde7 100644 --- a/core/src/main/java/net/corda/core/flows/IdentifiableException.java +++ b/core/src/main/java/net/corda/core/flows/IdentifiableException.java @@ -10,12 +10,15 @@ package net.corda.core.flows; +import net.corda.core.KeepForDJVM; + import javax.annotation.Nullable; /** * An exception that may be identified with an ID. If an exception originates in a counter-flow this ID will be * propagated. This allows correlation of error conditions across different flows. */ +@KeepForDJVM public interface IdentifiableException { /** * @return the ID of the error, or null if the error doesn't have it set (yet). diff --git a/core/src/main/kotlin/net/corda/core/ClientRelevantError.kt b/core/src/main/kotlin/net/corda/core/ClientRelevantError.kt index 5e8a6571ca..b9fa3879b5 100644 --- a/core/src/main/kotlin/net/corda/core/ClientRelevantError.kt +++ b/core/src/main/kotlin/net/corda/core/ClientRelevantError.kt @@ -16,4 +16,5 @@ import net.corda.core.serialization.CordaSerializable * Allows an implementing [Throwable] to be propagated to clients. */ @CordaSerializable +@KeepForDJVM interface ClientRelevantError \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/CordaException.kt b/core/src/main/kotlin/net/corda/core/CordaException.kt index bf4a16465a..40edbb5e9d 100644 --- a/core/src/main/kotlin/net/corda/core/CordaException.kt +++ b/core/src/main/kotlin/net/corda/core/CordaException.kt @@ -14,6 +14,7 @@ import net.corda.core.serialization.CordaSerializable import java.util.* @CordaSerializable +@KeepForDJVM interface CordaThrowable { var originalExceptionClassName: String? val originalMessage: String? @@ -22,6 +23,7 @@ interface CordaThrowable { fun addSuppressed(suppressed: Array) } +@KeepForDJVM open class CordaException internal constructor(override var originalExceptionClassName: String? = null, private var _message: String? = null, private var _cause: Throwable? = null) : Exception(null, null, true, true), CordaThrowable { @@ -69,6 +71,7 @@ open class CordaException internal constructor(override var originalExceptionCla } } +@KeepForDJVM open class CordaRuntimeException(override var originalExceptionClassName: String?, private var _message: String?, private var _cause: Throwable?) : RuntimeException(null, null, true, true), CordaThrowable { diff --git a/core/src/main/kotlin/net/corda/core/CordaInternal.kt b/core/src/main/kotlin/net/corda/core/CordaInternal.kt index 9bcc36ece3..f9e8342390 100644 --- a/core/src/main/kotlin/net/corda/core/CordaInternal.kt +++ b/core/src/main/kotlin/net/corda/core/CordaInternal.kt @@ -10,12 +10,15 @@ package net.corda.core +import kotlin.annotation.AnnotationRetention.* +import kotlin.annotation.AnnotationTarget.* + /** - * These methods are not part of Corda's API compatibility guarantee and applications should not use them. + * These methods and annotations are not part of Corda's API compatibility guarantee and applications should not use them. * - * These fields are only meant to be used by Corda internally, and are not intended to be part of the public API. + * These are only meant to be used by Corda internally, and are not intended to be part of the public API. */ -@Retention(AnnotationRetention.BINARY) -@Target(AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.FUNCTION) +@Target(PROPERTY_GETTER, PROPERTY_SETTER, FUNCTION, ANNOTATION_CLASS) +@Retention(BINARY) @MustBeDocumented annotation class CordaInternal \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/CordaOID.kt b/core/src/main/kotlin/net/corda/core/CordaOID.kt index 0682861acd..9c7fc32ae6 100644 --- a/core/src/main/kotlin/net/corda/core/CordaOID.kt +++ b/core/src/main/kotlin/net/corda/core/CordaOID.kt @@ -14,6 +14,7 @@ package net.corda.core * OIDs used for the Corda platform. Entries MUST NOT be removed from this file; if an OID is incorrectly assigned it * should be marked deprecated. */ +@KeepForDJVM object CordaOID { /** Assigned to R3, see http://www.oid-info.com/cgi-bin/display?oid=1.3.6.1.4.1.50530&action=display */ const val R3_ROOT = "1.3.6.1.4.1.50530" diff --git a/core/src/main/kotlin/net/corda/core/DeleteForDJVM.kt b/core/src/main/kotlin/net/corda/core/DeleteForDJVM.kt new file mode 100644 index 0000000000..36f94ac707 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/DeleteForDJVM.kt @@ -0,0 +1,26 @@ +package net.corda.core + +import kotlin.annotation.AnnotationRetention.* +import kotlin.annotation.AnnotationTarget.* +import kotlin.annotation.Retention +import kotlin.annotation.Target + +/** + * Declare the annotated element to unsuitable for the deterministic version of Corda. + */ +// DOCSTART 01 +@Target( + FILE, + CLASS, + CONSTRUCTOR, + FUNCTION, + PROPERTY_GETTER, + PROPERTY_SETTER, + PROPERTY, + FIELD, + TYPEALIAS +) +@Retention(BINARY) +@CordaInternal +annotation class DeleteForDJVM +// DOCEND 01 diff --git a/core/src/main/kotlin/net/corda/core/KeepForDJVM.kt b/core/src/main/kotlin/net/corda/core/KeepForDJVM.kt new file mode 100644 index 0000000000..0b25143247 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/KeepForDJVM.kt @@ -0,0 +1,19 @@ +package net.corda.core + +import kotlin.annotation.AnnotationRetention.* +import kotlin.annotation.AnnotationTarget.* +import kotlin.annotation.Retention +import kotlin.annotation.Target + +/** + * This annotates a class or file that we want to include into the deterministic version of Corda Core. + * We don't expect everything within that class/file to be deterministic; those non-deterministic + * elements need to be annotated with either [DeleteForDJVM] or [StubOutForDJVM] so that they + * can be deleted. + */ +// DOCSTART 01 +@Target(FILE, CLASS) +@Retention(BINARY) +@CordaInternal +annotation class KeepForDJVM +// DOCEND 01 \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/StubOutForDJVM.kt b/core/src/main/kotlin/net/corda/core/StubOutForDJVM.kt new file mode 100644 index 0000000000..d5e3fe7c5a --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/StubOutForDJVM.kt @@ -0,0 +1,24 @@ +package net.corda.core + +import kotlin.annotation.AnnotationRetention.* +import kotlin.annotation.AnnotationTarget.* +import kotlin.annotation.Retention +import kotlin.annotation.Target + +/** + * We expect that almost every non-deterministic element can have its bytecode + * deleted entirely from the deterministic version of Corda. This annotation is + * for those (hopefully!) few occasions where the non-deterministic function + * cannot be deleted. In these cases, the function will be stubbed out instead. + */ +// DOCSTART 01 +@Target( + CONSTRUCTOR, + FUNCTION, + PROPERTY_GETTER, + PROPERTY_SETTER +) +@Retention(BINARY) +@CordaInternal +annotation class StubOutForDJVM +// DOCEND 01 diff --git a/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt b/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt index 59451d8134..12fa8e806b 100644 --- a/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt +++ b/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt @@ -9,7 +9,6 @@ */ @file:JvmName("ConcurrencyUtils") - package net.corda.core.concurrent import net.corda.core.internal.concurrent.openFuture diff --git a/core/src/main/kotlin/net/corda/core/context/InvocationContext.kt b/core/src/main/kotlin/net/corda/core/context/InvocationContext.kt index 1ab15ecd8c..5a8653f272 100644 --- a/core/src/main/kotlin/net/corda/core/context/InvocationContext.kt +++ b/core/src/main/kotlin/net/corda/core/context/InvocationContext.kt @@ -10,6 +10,8 @@ package net.corda.core.context +import net.corda.core.DeleteForDJVM +import net.corda.core.KeepForDJVM import net.corda.core.contracts.ScheduledStateRef import net.corda.core.identity.CordaX500Name import net.corda.core.serialization.CordaSerializable @@ -31,36 +33,42 @@ data class InvocationContext(val origin: InvocationOrigin, val trace: Trace, val /** * Creates an [InvocationContext] with a [Trace] that defaults to a [java.util.UUID] as value and [java.time.Instant.now] timestamp. */ + @DeleteForDJVM @JvmStatic fun newInstance(origin: InvocationOrigin, trace: Trace = Trace.newInstance(), actor: Actor? = null, externalTrace: Trace? = null, impersonatedActor: Actor? = null) = InvocationContext(origin, trace, actor, externalTrace, impersonatedActor) /** * Creates an [InvocationContext] with [InvocationOrigin.RPC] origin. */ + @DeleteForDJVM @JvmStatic fun rpc(actor: Actor, trace: Trace = Trace.newInstance(), externalTrace: Trace? = null, impersonatedActor: Actor? = null): InvocationContext = newInstance(InvocationOrigin.RPC(actor), trace, actor, externalTrace, impersonatedActor) /** * Creates an [InvocationContext] with [InvocationOrigin.Peer] origin. */ + @DeleteForDJVM @JvmStatic fun peer(party: CordaX500Name, trace: Trace = Trace.newInstance(), externalTrace: Trace? = null, impersonatedActor: Actor? = null): InvocationContext = newInstance(InvocationOrigin.Peer(party), trace, null, externalTrace, impersonatedActor) /** * Creates an [InvocationContext] with [InvocationOrigin.Service] origin. */ + @DeleteForDJVM @JvmStatic fun service(serviceClassName: String, owningLegalIdentity: CordaX500Name, trace: Trace = Trace.newInstance(), externalTrace: Trace? = null): InvocationContext = newInstance(InvocationOrigin.Service(serviceClassName, owningLegalIdentity), trace, null, externalTrace) /** * Creates an [InvocationContext] with [InvocationOrigin.Scheduled] origin. */ + @DeleteForDJVM @JvmStatic fun scheduled(scheduledState: ScheduledStateRef, trace: Trace = Trace.newInstance(), externalTrace: Trace? = null): InvocationContext = newInstance(InvocationOrigin.Scheduled(scheduledState), trace, null, externalTrace) /** * Creates an [InvocationContext] with [InvocationOrigin.Shell] origin. */ + @DeleteForDJVM @JvmStatic fun shell(trace: Trace = Trace.newInstance(), externalTrace: Trace? = null): InvocationContext = InvocationContext(InvocationOrigin.Shell, trace, null, externalTrace) } @@ -74,6 +82,7 @@ data class InvocationContext(val origin: InvocationOrigin, val trace: Trace, val /** * Models an initiator in Corda, can be a user, a service, etc. */ +@KeepForDJVM @CordaSerializable data class Actor(val id: Id, val serviceId: AuthServiceId, val owningLegalIdentity: CordaX500Name) { @@ -85,6 +94,7 @@ data class Actor(val id: Id, val serviceId: AuthServiceId, val owningLegalIdenti /** * Actor id. */ + @KeepForDJVM @CordaSerializable data class Id(val value: String) } @@ -92,6 +102,7 @@ data class Actor(val id: Id, val serviceId: AuthServiceId, val owningLegalIdenti /** * Represents the source of an action such as a flow start, an RPC, a shell command etc. */ +@DeleteForDJVM @CordaSerializable sealed class InvocationOrigin { /** @@ -139,5 +150,6 @@ sealed class InvocationOrigin { /** * Authentication / Authorisation Service ID. */ +@KeepForDJVM @CordaSerializable data class AuthServiceId(val value: String) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/context/Trace.kt b/core/src/main/kotlin/net/corda/core/context/Trace.kt index de93fa9bd7..6dab5d11ef 100644 --- a/core/src/main/kotlin/net/corda/core/context/Trace.kt +++ b/core/src/main/kotlin/net/corda/core/context/Trace.kt @@ -10,6 +10,7 @@ package net.corda.core.context +import net.corda.core.DeleteForDJVM import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.Id import net.corda.core.utilities.UuidGenerator @@ -26,6 +27,7 @@ data class Trace(val invocationId: InvocationId, val sessionId: SessionId) { /** * Creates a trace using a [InvocationId.newInstance] with default arguments and a [SessionId] matching the value and timestamp from the invocation id.. */ + @DeleteForDJVM @JvmStatic fun newInstance(invocationId: InvocationId = InvocationId.newInstance(), sessionId: SessionId = SessionId(invocationId.value, invocationId.timestamp)) = Trace(invocationId, sessionId) } @@ -42,6 +44,7 @@ data class Trace(val invocationId: InvocationId, val sessionId: SessionId) { /** * Creates an invocation id using a [java.util.UUID] as value and [Instant.now] as timestamp. */ + @DeleteForDJVM @JvmStatic fun newInstance(value: String = UuidGenerator.next().toString(), timestamp: Instant = Instant.now()) = InvocationId(value, timestamp) } @@ -59,6 +62,7 @@ data class Trace(val invocationId: InvocationId, val sessionId: SessionId) { /** * Creates a session id using a [java.util.UUID] as value and [Instant.now] as timestamp. */ + @DeleteForDJVM @JvmStatic fun newInstance(value: String = UuidGenerator.next().toString(), timestamp: Instant = Instant.now()) = SessionId(value, timestamp) } diff --git a/core/src/main/kotlin/net/corda/core/contracts/Amount.kt b/core/src/main/kotlin/net/corda/core/contracts/Amount.kt index 2794deb617..6a688a6494 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Amount.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Amount.kt @@ -10,6 +10,7 @@ package net.corda.core.contracts +import net.corda.core.KeepForDJVM import net.corda.core.crypto.CompositeKey import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable @@ -46,6 +47,7 @@ interface TokenizableAssetInfo { * @property token the type of token this is an amount of. This is usually a singleton. * @param T the type of the token, for example [Currency]. T should implement [TokenizableAssetInfo] if automatic conversion to/from a display format is required. */ +@KeepForDJVM @CordaSerializable data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, val token: T) : Comparable> { // TODO Proper lookup of currencies in a locale and context sensitive fashion is not supported and is left to the application. diff --git a/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt b/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt index 13acc6d972..fee5bc6723 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt @@ -10,6 +10,7 @@ package net.corda.core.contracts +import net.corda.core.KeepForDJVM import net.corda.core.identity.Party import net.corda.core.internal.extractFile import net.corda.core.serialization.CordaSerializable @@ -37,6 +38,7 @@ import java.util.jar.JarInputStream * Finally, using ZIPs ensures files have a timestamp associated with them, and enables informational attachments * to be password protected (although in current releases password protected ZIPs are likely to fail to work). */ +@KeepForDJVM @CordaSerializable interface Attachment : NamedByHash { fun open(): InputStream diff --git a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt index 452ec2cafd..f557ad4c49 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt @@ -11,6 +11,7 @@ package net.corda.core.contracts import net.corda.core.DoNotImplement +import net.corda.core.KeepForDJVM import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint.isSatisfiedBy import net.corda.core.crypto.SecureHash import net.corda.core.internal.AttachmentWithContext @@ -26,6 +27,7 @@ interface AttachmentConstraint { } /** An [AttachmentConstraint] where [isSatisfiedBy] always returns true. */ +@KeepForDJVM object AlwaysAcceptAttachmentConstraint : AttachmentConstraint { override fun isSatisfiedBy(attachment: Attachment) = true } @@ -35,6 +37,7 @@ object AlwaysAcceptAttachmentConstraint : AttachmentConstraint { * The state protected by this constraint can only be used in a transaction created with that version of the jar. * And a receiving node will only accept it if a cordapp with that hash has (is) been deployed on the node. */ +@KeepForDJVM data class HashAttachmentConstraint(val attachmentId: SecureHash) : AttachmentConstraint { override fun isSatisfiedBy(attachment: Attachment): Boolean { return if (attachment is AttachmentWithContext) { @@ -48,6 +51,7 @@ data class HashAttachmentConstraint(val attachmentId: SecureHash) : AttachmentCo * See: [net.corda.core.node.NetworkParameters.whitelistedContractImplementations] * It allows for centralized control over the cordapps that can be used. */ +@KeepForDJVM object WhitelistedByZoneAttachmentConstraint : AttachmentConstraint { override fun isSatisfiedBy(attachment: Attachment): Boolean { return if (attachment is AttachmentWithContext) { @@ -67,6 +71,7 @@ object WhitelistedByZoneAttachmentConstraint : AttachmentConstraint { * intent of this class is that it should be replaced by a correct [HashAttachmentConstraint] and verify against an * actual [Attachment]. */ +@KeepForDJVM object AutomaticHashConstraint : AttachmentConstraint { override fun isSatisfiedBy(attachment: Attachment): Boolean { throw UnsupportedOperationException("Contracts cannot be satisfied by an AutomaticHashConstraint placeholder") diff --git a/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt b/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt index 01635c169e..f458c6b201 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt @@ -10,6 +10,7 @@ package net.corda.core.contracts +import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable /** @@ -19,6 +20,7 @@ import net.corda.core.serialization.CordaSerializable * @property contract The contract name contained within the JAR. A Contract attachment has to contain at least 1 contract. * @property additionalContracts Additional contract names contained within the JAR. */ +@KeepForDJVM @CordaSerializable class ContractAttachment @JvmOverloads constructor(val attachment: Attachment, val contract: ContractClassName, val additionalContracts: Set = emptySet(), val uploader: String? = null) : Attachment by attachment { diff --git a/core/src/main/kotlin/net/corda/core/contracts/ContractState.kt b/core/src/main/kotlin/net/corda/core/contracts/ContractState.kt index 3c6a3f1d3d..de961cbcdc 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractState.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractState.kt @@ -10,6 +10,7 @@ package net.corda.core.contracts +import net.corda.core.KeepForDJVM import net.corda.core.identity.AbstractParty import net.corda.core.serialization.CordaSerializable @@ -21,6 +22,7 @@ import net.corda.core.serialization.CordaSerializable * notary is responsible for ensuring there is no "double spending" by only signing a transaction if the input states * are all free. */ +@KeepForDJVM @CordaSerializable interface ContractState { /** diff --git a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt index 0a2c2e28a4..a84a8ce1d3 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt @@ -9,9 +9,10 @@ */ @file:JvmName("ContractsDSL") - +@file:KeepForDJVM package net.corda.core.contracts +import net.corda.core.KeepForDJVM import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.internal.uncheckedCast diff --git a/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt b/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt index 05652612f7..4e719a8946 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt @@ -10,6 +10,7 @@ package net.corda.core.contracts +import net.corda.core.KeepForDJVM import net.corda.core.flows.FlowException import net.corda.core.identity.AbstractParty import java.security.PublicKey @@ -30,6 +31,7 @@ class InsufficientBalanceException(val amountMissing: Amount<*>) : FlowException * @param T a type that represents the asset in question. This should describe the basic type of the asset * (GBP, USD, oil, shares in company , etc.) and any additional metadata (issuer, grade, class, etc.). */ +@KeepForDJVM interface FungibleAsset : OwnableState { /** * Amount represents a positive quantity of some issued product which can be cash, tokens, assets, or generally diff --git a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt index 95d58e96f6..604c347727 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt @@ -9,9 +9,11 @@ */ @file:JvmName("Structures") - +@file:KeepForDJVM package net.corda.core.contracts +import net.corda.core.DeleteForDJVM +import net.corda.core.KeepForDJVM import net.corda.core.crypto.SecureHash import net.corda.core.crypto.secureRandomBytes import net.corda.core.crypto.toStringShort @@ -53,6 +55,7 @@ interface NamedByHash { * of product may differentiate different kinds of asset within the same logical class e.g the currency, or * it may just be a type marker for a single custom asset. */ +@KeepForDJVM @CordaSerializable data class Issued(val issuer: PartyAndReference, val product: P) { init { @@ -79,11 +82,13 @@ fun Amount>.withoutIssuer(): Amount = Amount(quantity, di /** * Return structure for [OwnableState.withNewOwner] */ +@KeepForDJVM data class CommandAndState(val command: CommandData, val ownableState: OwnableState) /** * A contract state that can have a single owner. */ +@KeepForDJVM interface OwnableState : ContractState { /** There must be a MoveCommand signed by this key to claim the amount. */ val owner: AbstractParty @@ -120,6 +125,7 @@ data class ScheduledStateRef(val ref: StateRef, override val scheduledAt: Instan * for a particular [ContractState] have been processed/fired etc. If the activity is not "on ledger" then the * scheduled activity shouldn't be either. */ +@DeleteForDJVM data class ScheduledActivity(val logicRef: FlowLogicRef, override val scheduledAt: Instant) : Scheduled // DOCSTART 2 @@ -128,6 +134,7 @@ data class ScheduledActivity(val logicRef: FlowLogicRef, override val scheduledA * * This simplifies the job of tracking the current version of certain types of state in e.g. a vault. */ +@KeepForDJVM interface LinearState : ContractState { /** * Unique id shared by all LinearState states throughout history within the vaults of all parties. @@ -138,6 +145,7 @@ interface LinearState : ContractState { } // DOCEND 2 +@KeepForDJVM interface SchedulableState : ContractState { /** * Indicate whether there is some activity to be performed at some future point in time with respect to this @@ -148,6 +156,7 @@ interface SchedulableState : ContractState { * * @return null if there is no activity to schedule. */ + @DeleteForDJVM fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity? } @@ -158,6 +167,7 @@ fun ContractState.hash(): SecureHash = SecureHash.sha256(serialize().bytes) * A stateref is a pointer (reference) to a state, this is an equivalent of an "outpoint" in Bitcoin. It records which * transaction defined the state and where in that transaction it was. */ +@KeepForDJVM @CordaSerializable // DOCSTART 8 data class StateRef(val txhash: SecureHash, val index: Int) { @@ -166,6 +176,7 @@ data class StateRef(val txhash: SecureHash, val index: Int) { // DOCEND 8 /** A StateAndRef is simply a (state, ref) pair. For instance, a vault (which holds available assets) contains these. */ +@KeepForDJVM @CordaSerializable // DOCSTART 7 data class StateAndRef(val state: TransactionState, val ref: StateRef) @@ -180,6 +191,7 @@ inline fun Iterable>.filt * Reference to something being stored or issued by a party e.g. in a vault or (more likely) on their normal * ledger. The reference is intended to be encrypted so it's meaningless to anyone other than the party. */ +@KeepForDJVM @CordaSerializable data class PartyAndReference(val party: AbstractParty, val reference: OpaqueBytes) { override fun toString() = "$party$reference" @@ -190,12 +202,14 @@ data class PartyAndReference(val party: AbstractParty, val reference: OpaqueByte interface CommandData /** Commands that inherit from this are intended to have no data items: it's only their presence that matters. */ +@KeepForDJVM abstract class TypeOnlyCommandData : CommandData { override fun equals(other: Any?) = other?.javaClass == javaClass override fun hashCode() = javaClass.name.hashCode() } /** Command data/content plus pubkey pair: the signature is stored at the end of the serialized bytes */ +@KeepForDJVM @CordaSerializable data class Command(val value: T, val signers: List) { // TODO Introduce NonEmptyList? @@ -210,6 +224,7 @@ data class Command(val value: T, val signers: List) } /** A common move command for contract states which can change owner. */ +@KeepForDJVM interface MoveCommand : CommandData { /** * Contract code the moved state(s) are for the attention of, for example to indicate that the states are moved in @@ -221,6 +236,7 @@ interface MoveCommand : CommandData { // DOCSTART 6 /** A [Command] where the signing parties have been looked up if they have a well known/recognised institutional key. */ +@KeepForDJVM @CordaSerializable data class CommandWithParties( val signers: List, @@ -239,6 +255,7 @@ data class CommandWithParties( * * TODO: Contract serialization is likely to change, so the annotation is likely temporary. */ +@KeepForDJVM @CordaSerializable interface Contract { /** @@ -270,6 +287,7 @@ annotation class LegalProseReference(val uri: String) * more than one state). * @param NewState the upgraded contract state. */ +@KeepForDJVM interface UpgradedContract : Contract { /** * Name of the contract this is an upgraded version of, used as part of verification of upgrade transactions. @@ -288,6 +306,7 @@ interface UpgradedContract : UpgradedContract { /** * A validator for the legacy (pre-upgrade) contract attachments on the transaction. @@ -305,8 +324,10 @@ interface UpgradedContractWithLegacyConstraint) : TransactionVerificationException(txId, "Duplicate inputs: ${duplicates.joinToString()}", null) /** @suppress This class is obsolete and nothing has ever used it. */ @Deprecated("This class is obsolete and nothing has ever used it.") + @DeleteForDJVM class MoreThanOneNotary(txId: SecureHash) : TransactionVerificationException(txId, "More than one notary", null) /** @suppress This class is obsolete and nothing has ever used it. */ @Deprecated("This class is obsolete and nothing has ever used it.") + @DeleteForDJVM class SignersMissing(txId: SecureHash, val missing: List) : TransactionVerificationException(txId, "Signers missing: ${missing.joinToString()}", null) /** @suppress This class is obsolete and nothing has ever used it. */ @Deprecated("This class is obsolete and nothing has ever used it.") + @DeleteForDJVM class InvalidNotaryChange(txId: SecureHash) : TransactionVerificationException(txId, "Detected a notary change. Outputs must use the same notary as inputs", null) } diff --git a/core/src/main/kotlin/net/corda/core/contracts/UniqueIdentifier.kt b/core/src/main/kotlin/net/corda/core/contracts/UniqueIdentifier.kt index 9d2c4ecd2a..5f6e95adeb 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/UniqueIdentifier.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/UniqueIdentifier.kt @@ -10,6 +10,8 @@ package net.corda.core.contracts +import net.corda.core.DeleteForDJVM +import net.corda.core.KeepForDJVM import net.corda.core.internal.VisibleForTesting import net.corda.core.serialization.CordaSerializable import java.util.* @@ -27,7 +29,11 @@ import java.util.* * Subsequent copies and evolutions of a state should just copy the [externalId] and [id] fields unmodified. */ @CordaSerializable -data class UniqueIdentifier(val externalId: String? = null, val id: UUID = UUID.randomUUID()) : Comparable { +@KeepForDJVM +data class UniqueIdentifier(val externalId: String?, val id: UUID) : Comparable { + @DeleteForDJVM constructor(externalId: String?) : this(externalId, UUID.randomUUID()) + @DeleteForDJVM constructor() : this(null) + override fun toString(): String = if (externalId != null) "${externalId}_$id" else id.toString() companion object { diff --git a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt index b72b921410..441b93e1da 100644 --- a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt +++ b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt @@ -10,6 +10,7 @@ package net.corda.core.cordapp +import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic @@ -41,6 +42,7 @@ import java.net.URL * @property jarHash Hash of the jar */ @DoNotImplement +@DeleteForDJVM interface Cordapp { val name: String val contractClassNames: List diff --git a/core/src/main/kotlin/net/corda/core/cordapp/CordappContext.kt b/core/src/main/kotlin/net/corda/core/cordapp/CordappContext.kt index 61808239dd..deda274ec2 100644 --- a/core/src/main/kotlin/net/corda/core/cordapp/CordappContext.kt +++ b/core/src/main/kotlin/net/corda/core/cordapp/CordappContext.kt @@ -10,6 +10,7 @@ package net.corda.core.cordapp +import net.corda.core.DeleteForDJVM import net.corda.core.crypto.SecureHash /** @@ -25,6 +26,7 @@ import net.corda.core.crypto.SecureHash * @property classLoader the classloader used to load this cordapp's classes * @property config Configuration for this CorDapp */ +@DeleteForDJVM class CordappContext internal constructor( val cordapp: Cordapp, val attachmentId: SecureHash?, diff --git a/core/src/main/kotlin/net/corda/core/cordapp/CordappProvider.kt b/core/src/main/kotlin/net/corda/core/cordapp/CordappProvider.kt index c4c371f72a..a9b2e59b5f 100644 --- a/core/src/main/kotlin/net/corda/core/cordapp/CordappProvider.kt +++ b/core/src/main/kotlin/net/corda/core/cordapp/CordappProvider.kt @@ -10,6 +10,7 @@ package net.corda.core.cordapp +import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement import net.corda.core.contracts.ContractClassName import net.corda.core.node.services.AttachmentId @@ -18,6 +19,7 @@ import net.corda.core.node.services.AttachmentId * Provides access to what the node knows about loaded applications. */ @DoNotImplement +@DeleteForDJVM interface CordappProvider { /** * Exposes the current CorDapp context which will contain information and configuration of the CorDapp that diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt index 64ee55f798..5656ac0b91 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt @@ -10,6 +10,8 @@ package net.corda.core.crypto +import net.corda.core.DeleteForDJVM +import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.exactAdd import net.corda.core.utilities.sequence @@ -36,6 +38,7 @@ import java.util.* * @property threshold specifies the minimum total weight required (in the simple case – the minimum number of child * signatures required) to satisfy the sub-tree rooted at this node. */ +@KeepForDJVM @CordaSerializable class CompositeKey private constructor(val threshold: Int, children: List) : PublicKey { companion object { @@ -153,6 +156,7 @@ class CompositeKey private constructor(val threshold: Int, children: List, ASN1Object() { init { @@ -251,6 +255,7 @@ class CompositeKey private constructor(val threshold: Int, children: List = mutableListOf() diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeKeyFactory.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeKeyFactory.kt index 4381cfd106..afe54b2e59 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeKeyFactory.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeKeyFactory.kt @@ -10,6 +10,7 @@ package net.corda.core.crypto +import net.corda.core.DeleteForDJVM import java.security.* import java.security.spec.InvalidKeySpecException import java.security.spec.KeySpec @@ -18,6 +19,7 @@ import java.security.spec.X509EncodedKeySpec /** * Factory for generating composite keys from ASN.1 format key specifications. This is used by [CordaSecurityProvider]. */ +@DeleteForDJVM class CompositeKeyFactory : KeyFactorySpi() { @Throws(InvalidKeySpecException::class) diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt index a3acc67ebf..f4b46f601a 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt @@ -10,6 +10,7 @@ package net.corda.core.crypto +import net.corda.core.KeepForDJVM import net.corda.core.serialization.deserialize import java.io.ByteArrayOutputStream import java.security.* @@ -18,6 +19,7 @@ import java.security.spec.AlgorithmParameterSpec /** * Dedicated class for storing a set of signatures that comprise [CompositeKey]. */ +@KeepForDJVM class CompositeSignature : Signature(SIGNATURE_ALGORITHM) { companion object { const val SIGNATURE_ALGORITHM = "COMPOSITESIG" diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt index 36189b4217..60b389c8df 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt @@ -10,6 +10,7 @@ package net.corda.core.crypto +import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable /** @@ -17,6 +18,7 @@ import net.corda.core.serialization.CordaSerializable * serialization format. */ @CordaSerializable +@KeepForDJVM data class CompositeSignaturesWithKeys(val sigs: List) { companion object { val EMPTY = CompositeSignaturesWithKeys(emptyList()) diff --git a/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt b/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt index fd0ccb7cae..cbf294536d 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt @@ -10,38 +10,46 @@ package net.corda.core.crypto +import io.netty.util.concurrent.FastThreadLocal +import net.corda.core.KeepForDJVM +import net.corda.core.StubOutForDJVM import net.corda.core.crypto.CordaObjectIdentifier.COMPOSITE_KEY import net.corda.core.crypto.CordaObjectIdentifier.COMPOSITE_SIGNATURE import net.corda.core.internal.VisibleForTesting import org.bouncycastle.asn1.ASN1ObjectIdentifier -import io.netty.util.concurrent.FastThreadLocal import java.security.Provider import java.security.SecureRandom import java.security.SecureRandomSpi internal const val CORDA_SECURE_RANDOM_ALGORITHM = "CordaPRNG" +@KeepForDJVM class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME security provider wrapper") { companion object { const val PROVIDER_NAME = "Corda" } init { - put("KeyFactory.${CompositeKey.KEY_ALGORITHM}", CompositeKeyFactory::class.java.name) + provideNonDeterministic(this) put("Signature.${CompositeSignature.SIGNATURE_ALGORITHM}", CompositeSignature::class.java.name) - put("Alg.Alias.KeyFactory.$COMPOSITE_KEY", CompositeKey.KEY_ALGORITHM) - put("Alg.Alias.KeyFactory.OID.$COMPOSITE_KEY", CompositeKey.KEY_ALGORITHM) put("Alg.Alias.Signature.$COMPOSITE_SIGNATURE", CompositeSignature.SIGNATURE_ALGORITHM) put("Alg.Alias.Signature.OID.$COMPOSITE_SIGNATURE", CompositeSignature.SIGNATURE_ALGORITHM) - setSecureRandomService() - } - - private fun setSecureRandomService() { // Assuming this Provider is the first SecureRandom Provider, this algorithm is the SecureRandom default: putService(DelegatingSecureRandomService(this)) } } +/** + * The core-deterministic module is not allowed to generate keys. + */ +@StubOutForDJVM +private fun provideNonDeterministic(provider: Provider) { + provider["KeyFactory.${CompositeKey.KEY_ALGORITHM}"] = CompositeKeyFactory::class.java.name + provider["Alg.Alias.KeyFactory.$COMPOSITE_KEY"] = CompositeKey.KEY_ALGORITHM + provider["Alg.Alias.KeyFactory.OID.$COMPOSITE_KEY"] = CompositeKey.KEY_ALGORITHM +} + +@KeepForDJVM object CordaObjectIdentifier { // UUID-based OID // TODO: Register for an OID space and issue our own shorter OID. diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index 639461b4f1..c3f9e7ae0d 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -10,6 +10,8 @@ package net.corda.core.crypto +import net.corda.core.DeleteForDJVM +import net.corda.core.KeepForDJVM import net.corda.core.crypto.internal.* import net.corda.core.serialization.serialize import net.i2p.crypto.eddsa.EdDSAEngine @@ -68,6 +70,7 @@ import javax.crypto.spec.SecretKeySpec *
  • SPHINCS256_SHA512 (SPHINCS-256 hash-based signature scheme using SHA512 as hash algorithm). * */ +@KeepForDJVM object Crypto { /** * RSA PKCS#1 signature scheme using SHA256 for message hashing. @@ -623,6 +626,7 @@ object Crypto { * @return a KeyPair for the requested signature scheme code name. * @throws IllegalArgumentException if the requested signature scheme is not supported. */ + @DeleteForDJVM @JvmStatic fun generateKeyPair(schemeCodeName: String): KeyPair = generateKeyPair(findSignatureScheme(schemeCodeName)) @@ -633,6 +637,7 @@ object Crypto { * @return a new [KeyPair] for the requested [SignatureScheme]. * @throws IllegalArgumentException if the requested signature scheme is not supported. */ + @DeleteForDJVM @JvmOverloads @JvmStatic fun generateKeyPair(signatureScheme: SignatureScheme = DEFAULT_SIGNATURE_SCHEME): KeyPair { @@ -799,6 +804,7 @@ object Crypto { * @return a new [KeyPair] from an entropy input. * @throws IllegalArgumentException if the requested signature scheme is not supported for KeyPair generation using an entropy input. */ + @DeleteForDJVM @JvmStatic fun deriveKeyPairFromEntropy(signatureScheme: SignatureScheme, entropy: BigInteger): KeyPair { return when (signatureScheme) { @@ -814,6 +820,7 @@ object Crypto { * @param entropy a [BigInteger] value. * @return a new [KeyPair] from an entropy input. */ + @DeleteForDJVM @JvmStatic fun deriveKeyPairFromEntropy(entropy: BigInteger): KeyPair = deriveKeyPairFromEntropy(DEFAULT_SIGNATURE_SCHEME, entropy) diff --git a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt index 493a4c0b59..7fe8a0dd49 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -8,10 +8,13 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ +@file:KeepForDJVM @file:JvmName("CryptoUtils") package net.corda.core.crypto +import net.corda.core.DeleteForDJVM +import net.corda.core.KeepForDJVM import net.corda.core.contracts.PrivacySalt import net.corda.core.crypto.internal.platformSecureRandomFactory import net.corda.core.serialization.SerializationDefaults @@ -134,6 +137,7 @@ operator fun KeyPair.component1(): PrivateKey = this.private operator fun KeyPair.component2(): PublicKey = this.public /** A simple wrapper that will make it easier to swap out the signature algorithm we use in future. */ +@DeleteForDJVM fun generateKeyPair(): KeyPair = Crypto.generateKeyPair() /** @@ -142,6 +146,7 @@ fun generateKeyPair(): KeyPair = Crypto.generateKeyPair() * @param entropy a [BigInteger] value. * @return a deterministically generated [KeyPair] for the [Crypto.DEFAULT_SIGNATURE_SCHEME]. */ +@DeleteForDJVM fun entropyToKeyPair(entropy: BigInteger): KeyPair = Crypto.deriveKeyPairFromEntropy(entropy) /** @@ -178,6 +183,7 @@ fun KeyPair.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = Cr * or if no strong [SecureRandom] implementations are available or if Security.getProperty("securerandom.strongAlgorithms") is null or empty, * which should never happen and suggests an unusual JVM or non-standard Java library. */ +@DeleteForDJVM @Throws(NoSuchAlgorithmException::class) fun secureRandomBytes(numOfBytes: Int): ByteArray = ByteArray(numOfBytes).apply { newSecureRandom().nextBytes(this) } @@ -221,6 +227,7 @@ object DummySecureRandom : SecureRandom(DummySecureRandomSpi(), null) * or if no strong SecureRandom implementations are available or if Security.getProperty("securerandom.strongAlgorithms") is null or empty, * which should never happen and suggests an unusual JVM or non-standard Java library. */ +@DeleteForDJVM @Throws(NoSuchAlgorithmException::class) fun newSecureRandom(): SecureRandom = platformSecureRandomFactory() @@ -228,6 +235,7 @@ fun newSecureRandom(): SecureRandom = platformSecureRandomFactory() * Returns a random positive non-zero long generated using a secure RNG. This function sacrifies a bit of entropy in order * to avoid potential bugs where the value is used in a context where negative numbers or zero are not expected. */ +@DeleteForDJVM fun random63BitValue(): Long { while (true) { val candidate = Math.abs(newSecureRandom().nextLong()) diff --git a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt index 82cce461cb..01ff5a64d5 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt @@ -10,6 +10,7 @@ package net.corda.core.crypto +import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.OpaqueBytes import java.security.InvalidKeyException @@ -21,8 +22,10 @@ import java.security.SignatureException // should be renamed to match. /** A wrapper around a digital signature. */ @CordaSerializable +@KeepForDJVM open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) { /** A digital signature that identifies who the public key is owned by. */ + @KeepForDJVM open class WithKey(val by: PublicKey, bytes: ByteArray) : DigitalSignature(bytes) { /** * Utility to simplify the act of verifying a signature. diff --git a/core/src/main/kotlin/net/corda/core/crypto/MerkleTree.kt b/core/src/main/kotlin/net/corda/core/crypto/MerkleTree.kt index 850d4c5cf2..45d6827717 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/MerkleTree.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/MerkleTree.kt @@ -10,6 +10,7 @@ package net.corda.core.crypto +import net.corda.core.KeepForDJVM import java.util.* /** @@ -24,8 +25,8 @@ import java.util.* sealed class MerkleTree { abstract val hash: SecureHash - data class Leaf(override val hash: SecureHash) : MerkleTree() - data class Node(override val hash: SecureHash, val left: MerkleTree, val right: MerkleTree) : MerkleTree() + @KeepForDJVM data class Leaf(override val hash: SecureHash) : MerkleTree() + @KeepForDJVM data class Node(override val hash: SecureHash, val left: MerkleTree, val right: MerkleTree) : MerkleTree() companion object { private fun isPow2(num: Int): Boolean = num and (num - 1) == 0 diff --git a/core/src/main/kotlin/net/corda/core/crypto/NullKeys.kt b/core/src/main/kotlin/net/corda/core/crypto/NullKeys.kt index 2bd1b81586..b6796e3bbf 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/NullKeys.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/NullKeys.kt @@ -10,10 +10,12 @@ package net.corda.core.crypto +import net.corda.core.KeepForDJVM import net.corda.core.identity.AnonymousParty import net.corda.core.serialization.CordaSerializable import java.security.PublicKey +@KeepForDJVM object NullKeys { @CordaSerializable object NullPublicKey : PublicKey, Comparable { diff --git a/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt b/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt index af21b2d5da..b34653825c 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt @@ -11,10 +11,12 @@ package net.corda.core.crypto import net.corda.core.CordaException +import net.corda.core.KeepForDJVM import net.corda.core.crypto.SecureHash.Companion.zeroHash import net.corda.core.serialization.CordaSerializable import java.util.* +@KeepForDJVM @CordaSerializable class MerkleTreeException(val reason: String) : CordaException("Partial Merkle Tree exception. Reason: $reason") @@ -52,6 +54,7 @@ class MerkleTreeException(val reason: String) : CordaException("Partial Merkle T * (there can be a difference in obtained leaves ordering - that's why it's a set comparison not hashing leaves into a tree). * If both equalities hold, we can assume that l3 and l5 belong to the transaction with root h15. */ +@KeepForDJVM @CordaSerializable class PartialMerkleTree(val root: PartialTree) { /** @@ -63,9 +66,9 @@ class PartialMerkleTree(val root: PartialTree) { */ @CordaSerializable sealed class PartialTree { - data class IncludedLeaf(val hash: SecureHash) : PartialTree() - data class Leaf(val hash: SecureHash) : PartialTree() - data class Node(val left: PartialTree, val right: PartialTree) : PartialTree() + @KeepForDJVM data class IncludedLeaf(val hash: SecureHash) : PartialTree() + @KeepForDJVM data class Leaf(val hash: SecureHash) : PartialTree() + @KeepForDJVM data class Node(val left: PartialTree, val right: PartialTree) : PartialTree() } companion object { diff --git a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt index 091dff47c1..4eae2b2356 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt @@ -8,9 +8,12 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ +@file:KeepForDJVM package net.corda.core.crypto import io.netty.util.concurrent.FastThreadLocal +import net.corda.core.DeleteForDJVM +import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.parseAsHex @@ -22,6 +25,7 @@ import java.util.function.Supplier * Container for a cryptographically secure hash value. * Provides utilities for generating a cryptographic hash using different algorithms (currently only SHA-256 supported). */ +@KeepForDJVM @CordaSerializable sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) { /** SHA-256 is part of the SHA-2 hash function family. Generated hash is fixed size, 256-bits (32-bytes). */ @@ -91,6 +95,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) { /** * Generates a random SHA-256 value. */ + @DeleteForDJVM @JvmStatic fun randomSHA256() = sha256(secureRandomBytes(32)) diff --git a/core/src/main/kotlin/net/corda/core/crypto/SignableData.kt b/core/src/main/kotlin/net/corda/core/crypto/SignableData.kt index 3c9ca05323..6dd5eccff6 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SignableData.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SignableData.kt @@ -10,6 +10,7 @@ package net.corda.core.crypto +import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable /** @@ -22,4 +23,5 @@ import net.corda.core.serialization.CordaSerializable * @param signatureMetadata meta data required. */ @CordaSerializable +@KeepForDJVM data class SignableData(val txId: SecureHash, val signatureMetadata: SignatureMetadata) diff --git a/core/src/main/kotlin/net/corda/core/crypto/SignatureMetadata.kt b/core/src/main/kotlin/net/corda/core/crypto/SignatureMetadata.kt index 3192d3f0c1..bc9e2963cc 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SignatureMetadata.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SignatureMetadata.kt @@ -10,6 +10,7 @@ package net.corda.core.crypto +import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable /** @@ -22,4 +23,5 @@ import net.corda.core.serialization.CordaSerializable * @param schemeNumberID number id of the signature scheme used based on signer's key-pair, see [SignatureScheme.schemeNumberID]. */ @CordaSerializable +@KeepForDJVM data class SignatureMetadata(val platformVersion: Int, val schemeNumberID: Int) diff --git a/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt b/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt index 01d1eeced8..ace5010f7f 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt @@ -10,6 +10,7 @@ package net.corda.core.crypto +import net.corda.core.KeepForDJVM import org.bouncycastle.asn1.x509.AlgorithmIdentifier import java.security.Signature import java.security.spec.AlgorithmParameterSpec @@ -30,6 +31,7 @@ import java.security.spec.AlgorithmParameterSpec * @param keySize the private key size (currently used for RSA only). * @param desc a human-readable description for this scheme. */ +@KeepForDJVM data class SignatureScheme( val schemeNumberID: Int, val schemeCodeName: String, diff --git a/core/src/main/kotlin/net/corda/core/crypto/SignedData.kt b/core/src/main/kotlin/net/corda/core/crypto/SignedData.kt index 4bb8b5d3b7..d8f81bbd68 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SignedData.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SignedData.kt @@ -10,6 +10,7 @@ package net.corda.core.crypto +import net.corda.core.KeepForDJVM import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes @@ -24,6 +25,7 @@ import java.security.SignatureException * @param sig the (unverified) signature for the data. */ @CordaSerializable +@KeepForDJVM open class SignedData(val raw: SerializedBytes, val sig: DigitalSignature.WithKey) { /** * Return the deserialized data if the signature can be verified. diff --git a/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt index 9b2cb289a2..5fba7e02d8 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt @@ -10,6 +10,7 @@ package net.corda.core.crypto +import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable import java.security.InvalidKeyException import java.security.PublicKey @@ -25,6 +26,7 @@ import java.util.* * @property partialMerkleTree required when multi-transaction signing is utilised. */ @CordaSerializable +@KeepForDJVM class TransactionSignature(bytes: ByteArray, val by: PublicKey, val signatureMetadata: SignatureMetadata, val partialMerkleTree: PartialMerkleTree?) : DigitalSignature(bytes) { /** * Construct a [TransactionSignature] with [partialMerkleTree] set to null. diff --git a/core/src/main/kotlin/net/corda/core/crypto/internal/PlatformSecureRandom.kt b/core/src/main/kotlin/net/corda/core/crypto/internal/PlatformSecureRandom.kt index bc66484f4b..3213aaaedc 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/internal/PlatformSecureRandom.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/internal/PlatformSecureRandom.kt @@ -8,8 +8,10 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ @file:JvmName("PlatformSecureRandom") +@file:DeleteForDJVM package net.corda.core.crypto.internal +import net.corda.core.DeleteForDJVM import net.corda.core.crypto.CORDA_SECURE_RANDOM_ALGORITHM import net.corda.core.crypto.DummySecureRandom import net.corda.core.internal.VisibleForTesting diff --git a/core/src/main/kotlin/net/corda/core/crypto/internal/ProviderMap.kt b/core/src/main/kotlin/net/corda/core/crypto/internal/ProviderMap.kt index 6a01370629..a905ef0f1d 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/internal/ProviderMap.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/internal/ProviderMap.kt @@ -9,6 +9,7 @@ */ package net.corda.core.crypto.internal +import net.corda.core.DeleteForDJVM import net.corda.core.crypto.CordaSecurityProvider import net.corda.core.crypto.Crypto.EDDSA_ED25519_SHA512 import net.corda.core.crypto.Crypto.decodePrivateKey @@ -53,4 +54,5 @@ internal val bouncyCastlePQCProvider = BouncyCastlePQCProvider().apply { // The val is private to avoid any harmful state changes. internal val providerMap = listOf(cordaBouncyCastleProvider, cordaSecurityProvider, bouncyCastlePQCProvider).map { it.name to it }.toMap() +@DeleteForDJVM internal fun platformSecureRandomFactory() = platformSecureRandom // To minimise diff of CryptoUtils against open-source. diff --git a/core/src/main/kotlin/net/corda/core/flows/StateMachineRunId.kt b/core/src/main/kotlin/net/corda/core/flows/StateMachineRunId.kt index 4175b223fd..db56f7b7a3 100644 --- a/core/src/main/kotlin/net/corda/core/flows/StateMachineRunId.kt +++ b/core/src/main/kotlin/net/corda/core/flows/StateMachineRunId.kt @@ -10,6 +10,7 @@ package net.corda.core.flows +import net.corda.core.DeleteForDJVM import net.corda.core.serialization.CordaSerializable import java.util.* @@ -20,6 +21,7 @@ import java.util.* @CordaSerializable data class StateMachineRunId(val uuid: UUID) { companion object { + @DeleteForDJVM fun createRandom(): StateMachineRunId = StateMachineRunId(UUID.randomUUID()) } diff --git a/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt b/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt index 0fd5692912..9acb81265b 100644 --- a/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt +++ b/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt @@ -10,6 +10,7 @@ package net.corda.core.identity +import net.corda.core.KeepForDJVM import net.corda.core.contracts.PartyAndReference import net.corda.core.crypto.toStringShort import net.corda.core.utilities.OpaqueBytes @@ -19,6 +20,7 @@ import java.security.PublicKey * The [AnonymousParty] class contains enough information to uniquely identify a [Party] while excluding private * information such as name. It is intended to represent a party on the distributed ledger. */ +@KeepForDJVM class AnonymousParty(owningKey: PublicKey) : AbstractParty(owningKey) { override fun nameOrNull(): CordaX500Name? = null override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes) diff --git a/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt b/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt index 57b80a45ba..cf9ad72a9d 100644 --- a/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt +++ b/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt @@ -11,6 +11,7 @@ package net.corda.core.identity import com.google.common.collect.ImmutableSet +import net.corda.core.KeepForDJVM import net.corda.core.internal.LegalNameValidator import net.corda.core.internal.unspecifiedCountry import net.corda.core.internal.x500Name @@ -40,6 +41,7 @@ import javax.security.auth.x500.X500Principal * attribute type. */ @CordaSerializable +@KeepForDJVM data class CordaX500Name(val commonName: String?, val organisationUnit: String?, val organisation: String, diff --git a/core/src/main/kotlin/net/corda/core/identity/Party.kt b/core/src/main/kotlin/net/corda/core/identity/Party.kt index 75604dbc9b..6036e7d237 100644 --- a/core/src/main/kotlin/net/corda/core/identity/Party.kt +++ b/core/src/main/kotlin/net/corda/core/identity/Party.kt @@ -10,6 +10,7 @@ package net.corda.core.identity +import net.corda.core.KeepForDJVM import net.corda.core.contracts.PartyAndReference import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.Crypto @@ -36,6 +37,7 @@ import java.security.cert.X509Certificate * * @see CompositeKey */ +@KeepForDJVM class Party(val name: CordaX500Name, owningKey: PublicKey) : AbstractParty(owningKey) { constructor(certificate: X509Certificate) : this(CordaX500Name.build(certificate.subjectX500Principal), Crypto.toSupportedPublicKey(certificate.publicKey)) diff --git a/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt b/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt index 1cf22f7c66..cf30f6bd49 100644 --- a/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt +++ b/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt @@ -10,6 +10,7 @@ package net.corda.core.identity +import net.corda.core.KeepForDJVM import net.corda.core.internal.CertRole import net.corda.core.internal.uncheckedCast import net.corda.core.internal.validate @@ -23,6 +24,7 @@ import java.security.cert.* * not part of the identifier themselves. */ @CordaSerializable +@KeepForDJVM class PartyAndCertificate(val certPath: CertPath) { @Transient val certificate: X509Certificate diff --git a/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt b/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt index bb8b125d8f..33574c27ee 100644 --- a/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt +++ b/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt @@ -8,8 +8,11 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ +@file:KeepForDJVM package net.corda.core.internal +import net.corda.core.DeleteForDJVM +import net.corda.core.KeepForDJVM import net.corda.core.contracts.Attachment import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party @@ -33,8 +36,10 @@ const val UNKNOWN_UPLOADER = "unknown" fun isUploaderTrusted(uploader: String?) = uploader?.let { it in listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER, TEST_UPLOADER) } ?: false +@KeepForDJVM abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment { companion object { + @DeleteForDJVM fun SerializeAsTokenContext.attachmentDataLoader(id: SecureHash): () -> ByteArray { return { val a = serviceHub.attachments.openAttachment(id) ?: throw MissingAttachmentsException(listOf(id)) diff --git a/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt b/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt index f1cf6ba89e..84a0e2a8ad 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt @@ -10,11 +10,13 @@ package net.corda.core.internal +import net.corda.core.DeleteForDJVM import net.corda.core.contracts.* import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentId import net.corda.core.transactions.ContractUpgradeWireTransaction +@DeleteForDJVM object ContractUpgradeUtils { fun assembleUpgradeTx( stateAndRef: StateAndRef, diff --git a/core/src/main/kotlin/net/corda/core/internal/Emoji.kt b/core/src/main/kotlin/net/corda/core/internal/Emoji.kt index 5f773fe82d..db6bf99fe5 100644 --- a/core/src/main/kotlin/net/corda/core/internal/Emoji.kt +++ b/core/src/main/kotlin/net/corda/core/internal/Emoji.kt @@ -10,9 +10,12 @@ package net.corda.core.internal +import net.corda.core.DeleteForDJVM + /** * A simple wrapper class that contains icons and support for printing them only when we're connected to a terminal. */ +@DeleteForDJVM object Emoji { // Unfortunately only Apple has a terminal that can do colour emoji AND an emoji font installed by default. // However the JediTerm java terminal emulator can also do emoji on OS X when using the JetBrains JRE. diff --git a/core/src/main/kotlin/net/corda/core/internal/FlowIORequest.kt b/core/src/main/kotlin/net/corda/core/internal/FlowIORequest.kt index e770327c02..535ff9aa07 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FlowIORequest.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FlowIORequest.kt @@ -10,6 +10,7 @@ package net.corda.core.internal +import net.corda.core.DeleteForDJVM import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowInfo import net.corda.core.flows.FlowSession @@ -21,6 +22,7 @@ import java.time.Instant /** * A [FlowIORequest] represents an IO request of a flow when it suspends. It is persisted in checkpoints. */ +@DeleteForDJVM sealed class FlowIORequest { /** * Send messages to sessions. diff --git a/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt b/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt index 1892b7ef71..d922d4dbf3 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt @@ -14,10 +14,7 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.DoNotImplement import net.corda.core.concurrent.CordaFuture import net.corda.core.context.InvocationContext -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowSession -import net.corda.core.flows.FlowStackSnapshot -import net.corda.core.flows.StateMachineRunId +import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import org.slf4j.Logger 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 c1755ee690..86ee517c35 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -9,11 +9,13 @@ */ @file:JvmName("InternalUtils") - +@file:KeepForDJVM package net.corda.core.internal import com.google.common.hash.Hashing import com.google.common.hash.HashingInputStream +import net.corda.core.DeleteForDJVM +import net.corda.core.KeepForDJVM import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.CordappConfig import net.corda.core.cordapp.CordappContext @@ -118,6 +120,7 @@ fun List.noneOrSingle(): T? { } /** Returns a random element in the list, or `null` if empty */ +@DeleteForDJVM fun List.randomOrNull(): T? { return when (size) { 0 -> null @@ -133,7 +136,7 @@ fun List.indexOfOrThrow(item: T): Int { return i } -fun InputStream.copyTo(target: Path, vararg options: CopyOption): Long = Files.copy(this, target, *options) +@DeleteForDJVM fun InputStream.copyTo(target: Path, vararg options: CopyOption): Long = Files.copy(this, target, *options) /** Same as [InputStream.readBytes] but also closes the stream. */ fun InputStream.readFully(): ByteArray = use { it.readBytes() } @@ -164,6 +167,7 @@ fun Iterable.sum(): BigDecimal = fold(BigDecimal.ZERO) { a, b -> a + * Returns an Observable that buffers events until subscribed. * @see UnicastSubject */ +@DeleteForDJVM fun Observable.bufferUntilSubscribed(): Observable { val subject = UnicastSubject.create() val subscription = subscribe(subject) @@ -171,6 +175,7 @@ fun Observable.bufferUntilSubscribed(): Observable { } /** Copy an [Observer] to multiple other [Observer]s. */ +@DeleteForDJVM fun Observer.tee(vararg teeTo: Observer): Observer { val subject = PublishSubject.create() subject.subscribe(this) @@ -179,6 +184,7 @@ fun Observer.tee(vararg teeTo: Observer): Observer { } /** Executes the given code block and returns a [Duration] of how long it took to execute in nanosecond precision. */ +@DeleteForDJVM inline fun elapsedTime(block: () -> Unit): Duration { val start = System.nanoTime() block() @@ -191,6 +197,7 @@ fun Logger.logElapsedTime(label: String, body: () -> T): T = logElapsedTime( // TODO: Add inline back when a new Kotlin version is released and check if the java.lang.VerifyError // returns in the IRSSimulationTest. If not, commit the inline back. +@DeleteForDJVM fun logElapsedTime(label: String, logger: Logger? = null, body: () -> T): T { // Use nanoTime as it's monotonic. val now = System.nanoTime() @@ -218,6 +225,7 @@ fun ByteArrayOutputStream.toInputStreamAndHash(): InputStreamAndHash { return InputStreamAndHash(bytes.inputStream(), bytes.sha256()) } +@KeepForDJVM data class InputStreamAndHash(val inputStream: InputStream, val sha256: SecureHash.SHA256) { companion object { /** @@ -225,6 +233,7 @@ data class InputStreamAndHash(val inputStream: InputStream, val sha256: SecureHa * called "z" that contains the given content byte repeated the given number of times. * Note that a slightly bigger than numOfExpectedBytes size is expected. */ + @DeleteForDJVM fun createInMemoryTestZip(numOfExpectedBytes: Int, content: Byte): InputStreamAndHash { require(numOfExpectedBytes > 0) val baos = ByteArrayOutputStream() @@ -278,24 +287,29 @@ inline fun Stream.mapNotNull(crossinline transform: (T) -> R?): fun Class.castIfPossible(obj: Any): T? = if (isInstance(obj)) cast(obj) else null /** Returns a [DeclaredField] wrapper around the declared (possibly non-public) static field of the receiver [Class]. */ +@DeleteForDJVM fun Class<*>.staticField(name: String): DeclaredField = DeclaredField(this, name, null) /** Returns a [DeclaredField] wrapper around the declared (possibly non-public) static field of the receiver [KClass]. */ +@DeleteForDJVM fun KClass<*>.staticField(name: String): DeclaredField = DeclaredField(java, name, null) /** Returns a [DeclaredField] wrapper around the declared (possibly non-public) instance field of the receiver object. */ +@DeleteForDJVM fun Any.declaredField(name: String): DeclaredField = DeclaredField(javaClass, name, this) /** * Returns a [DeclaredField] wrapper around the (possibly non-public) instance field of the receiver object, but declared * in its superclass [clazz]. */ +@DeleteForDJVM fun Any.declaredField(clazz: KClass<*>, name: String): DeclaredField = DeclaredField(clazz.java, name, this) /** * Returns a [DeclaredField] wrapper around the (possibly non-public) instance field of the receiver object, but declared * in its superclass [clazz]. */ +@DeleteForDJVM fun Any.declaredField(clazz: Class<*>, name: String): DeclaredField = DeclaredField(clazz, name, this) /** creates a new instance if not a Kotlin object */ @@ -324,6 +338,7 @@ val Class.kotlinObjectInstance: T? get() { * A simple wrapper around a [Field] object providing type safe read and write access using [value], ignoring the field's * visibility. */ +@DeleteForDJVM class DeclaredField(clazz: Class<*>, name: String, private val receiver: Any?) { private val javaField = findField(name, clazz) var value: T @@ -380,12 +395,12 @@ fun uncheckedCast(obj: T) = obj as U fun Iterable>.toMultiMap(): Map> = this.groupBy({ it.first }) { it.second } /** Provide access to internal method for AttachmentClassLoaderTests */ -fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serializationContext: SerializationContext): WireTransaction { +@DeleteForDJVM fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serializationContext: SerializationContext): WireTransaction { return toWireTransactionWithContext(services, serializationContext) } /** Provide access to internal method for AttachmentClassLoaderTests */ -fun TransactionBuilder.toLedgerTransaction(services: ServicesForResolution, serializationContext: SerializationContext) = toLedgerTransactionWithContext(services, serializationContext) +@DeleteForDJVM fun TransactionBuilder.toLedgerTransaction(services: ServicesForResolution, serializationContext: SerializationContext) = toLedgerTransactionWithContext(services, serializationContext) /** Convenience method to get the package name of a class literal. */ val KClass<*>.packageName: String get() = java.packageName @@ -401,12 +416,14 @@ inline val Member.isStatic: Boolean get() = Modifier.isStatic(modifiers) inline val Member.isFinal: Boolean get() = Modifier.isFinal(modifiers) -fun URI.toPath(): Path = Paths.get(this) +@DeleteForDJVM fun URI.toPath(): Path = Paths.get(this) -fun URL.toPath(): Path = toURI().toPath() +@DeleteForDJVM fun URL.toPath(): Path = toURI().toPath() +@DeleteForDJVM fun URL.openHttpConnection(): HttpURLConnection = openConnection() as HttpURLConnection +@DeleteForDJVM fun URL.post(serializedData: OpaqueBytes, vararg properties: Pair): ByteArray { return openHttpConnection().run { doOutput = true @@ -419,20 +436,24 @@ fun URL.post(serializedData: OpaqueBytes, vararg properties: Pair HttpURLConnection.responseAs(): T { checkOkResponse() return inputStream.readObject() } /** Analogous to [Thread.join]. */ +@DeleteForDJVM fun ExecutorService.join() { shutdown() // Do not change to shutdownNow, tests use this method to assert the executor has no more tasks. while (!awaitTermination(1, TimeUnit.SECONDS)) { diff --git a/core/src/main/kotlin/net/corda/core/internal/LazyPool.kt b/core/src/main/kotlin/net/corda/core/internal/LazyPool.kt index e44a14d16f..bee51d5e29 100644 --- a/core/src/main/kotlin/net/corda/core/internal/LazyPool.kt +++ b/core/src/main/kotlin/net/corda/core/internal/LazyPool.kt @@ -10,6 +10,7 @@ package net.corda.core.internal +import net.corda.core.DeleteForDJVM import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.Semaphore @@ -24,6 +25,7 @@ import java.util.concurrent.Semaphore * instance is released. * @param newInstance The function to call to lazily newInstance a pooled resource. */ +@DeleteForDJVM class LazyPool( private val clear: ((A) -> Unit)? = null, private val shouldReturnToPool: ((A) -> Boolean)? = null, diff --git a/core/src/main/kotlin/net/corda/core/internal/LazyStickyPool.kt b/core/src/main/kotlin/net/corda/core/internal/LazyStickyPool.kt index 81e9955b49..c425d8069e 100644 --- a/core/src/main/kotlin/net/corda/core/internal/LazyStickyPool.kt +++ b/core/src/main/kotlin/net/corda/core/internal/LazyStickyPool.kt @@ -10,6 +10,7 @@ package net.corda.core.internal +import net.corda.core.DeleteForDJVM import java.util.* import java.util.concurrent.LinkedBlockingQueue @@ -21,6 +22,7 @@ import java.util.concurrent.LinkedBlockingQueue * @param newInstance The function to call to create a pooled resource. */ // TODO This could be implemented more efficiently. Currently the "non-sticky" use case is not optimised, it just chooses a random instance to wait on. +@DeleteForDJVM class LazyStickyPool( size: Int, private val newInstance: () -> A diff --git a/core/src/main/kotlin/net/corda/core/internal/LegalNameValidator.kt b/core/src/main/kotlin/net/corda/core/internal/LegalNameValidator.kt index 2840209a05..10d79f1d6d 100644 --- a/core/src/main/kotlin/net/corda/core/internal/LegalNameValidator.kt +++ b/core/src/main/kotlin/net/corda/core/internal/LegalNameValidator.kt @@ -10,6 +10,7 @@ package net.corda.core.internal +import net.corda.core.KeepForDJVM import net.corda.core.internal.LegalNameValidator.normalize import java.text.Normalizer import javax.security.auth.x500.X500Principal @@ -115,12 +116,14 @@ object LegalNameValidator { abstract fun validate(legalName: T) + @KeepForDJVM private class UnicodeNormalizationRule : Rule() { override fun validate(legalName: String) { require(legalName == normalize(legalName)) { "Legal name must be normalized. Please use 'normalize' to normalize the legal name before validation." } } } + @KeepForDJVM private class UnicodeRangeRule(vararg supportScripts: Character.UnicodeBlock) : Rule() { val supportScriptsSet = supportScripts.toSet() @@ -131,6 +134,7 @@ object LegalNameValidator { } } + @KeepForDJVM private class CharacterRule(vararg val bannedChars: Char) : Rule() { override fun validate(legalName: String) { bannedChars.forEach { @@ -139,6 +143,7 @@ object LegalNameValidator { } } + @KeepForDJVM private class WordRule(vararg val bannedWords: String) : Rule() { override fun validate(legalName: String) { bannedWords.forEach { @@ -147,12 +152,14 @@ object LegalNameValidator { } } + @KeepForDJVM private class LengthRule(val maxLength: Int) : Rule() { override fun validate(legalName: String) { require(legalName.length <= maxLength) { "Legal name longer then $maxLength characters." } } } + @KeepForDJVM private class CapitalLetterRule : Rule() { override fun validate(legalName: String) { val capitalizedLegalName = legalName.capitalize() @@ -160,6 +167,7 @@ object LegalNameValidator { } } + @KeepForDJVM private class X500NameRule : Rule() { override fun validate(legalName: String) { // This will throw IllegalArgumentException if the name does not comply with X500 name format. @@ -167,6 +175,7 @@ object LegalNameValidator { } } + @KeepForDJVM private class MustHaveAtLeastTwoLettersRule : Rule() { override fun validate(legalName: String) { // Try to exclude names like "/", "£", "X" etc. diff --git a/core/src/main/kotlin/net/corda/core/internal/LifeCycle.kt b/core/src/main/kotlin/net/corda/core/internal/LifeCycle.kt index e7e08908f7..b76d96b294 100644 --- a/core/src/main/kotlin/net/corda/core/internal/LifeCycle.kt +++ b/core/src/main/kotlin/net/corda/core/internal/LifeCycle.kt @@ -10,6 +10,7 @@ package net.corda.core.internal +import net.corda.core.DeleteForDJVM import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.withLock @@ -19,6 +20,7 @@ import kotlin.concurrent.withLock * * @param initial The initial state. */ +@DeleteForDJVM class LifeCycle>(initial: S) { private val lock = ReentrantReadWriteLock() private var state = initial diff --git a/core/src/main/kotlin/net/corda/core/internal/PathUtils.kt b/core/src/main/kotlin/net/corda/core/internal/PathUtils.kt index 2b60d96ff1..43a681eb57 100644 --- a/core/src/main/kotlin/net/corda/core/internal/PathUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/PathUtils.kt @@ -1,5 +1,7 @@ +@file:DeleteForDJVM package net.corda.core.internal +import net.corda.core.DeleteForDJVM import net.corda.core.crypto.SecureHash import net.corda.core.serialization.deserialize import java.io.* diff --git a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt index 496c76112f..bbf96e9a0a 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt @@ -11,6 +11,7 @@ package net.corda.core.internal import co.paralleluniverse.fibers.Suspendable +import net.corda.core.DeleteForDJVM import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic @@ -32,6 +33,7 @@ import kotlin.math.min * * @return a list of verified [SignedTransaction] objects, in a depth-first order. */ +@DeleteForDJVM class ResolveTransactionsFlow(txHashesArg: Set, private val otherSide: FlowSession) : FlowLogic() { @@ -48,6 +50,7 @@ class ResolveTransactionsFlow(txHashesArg: Set, this.signedTransaction = signedTransaction } + @DeleteForDJVM companion object { private fun dependencyIDs(stx: SignedTransaction) = stx.inputs.map { it.txhash }.toSet() diff --git a/core/src/main/kotlin/net/corda/core/internal/ThreadBox.kt b/core/src/main/kotlin/net/corda/core/internal/ThreadBox.kt index 03712b2e6c..284524be0e 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ThreadBox.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ThreadBox.kt @@ -10,6 +10,7 @@ package net.corda.core.internal +import net.corda.core.DeleteForDJVM import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -31,6 +32,7 @@ import kotlin.concurrent.withLock * val ii = state.locked { i } * ``` */ +@DeleteForDJVM class ThreadBox(val content: T, val lock: ReentrantLock = ReentrantLock()) { inline fun locked(body: T.() -> R): R = lock.withLock { body(content) } inline fun alreadyLocked(body: T.() -> R): R { diff --git a/core/src/main/kotlin/net/corda/core/internal/X509EdDSAEngine.kt b/core/src/main/kotlin/net/corda/core/internal/X509EdDSAEngine.kt index 41f0ff07bf..62c4bb9d98 100644 --- a/core/src/main/kotlin/net/corda/core/internal/X509EdDSAEngine.kt +++ b/core/src/main/kotlin/net/corda/core/internal/X509EdDSAEngine.kt @@ -10,6 +10,7 @@ package net.corda.core.internal +import net.corda.core.DeleteForDJVM import net.i2p.crypto.eddsa.EdDSAEngine import net.i2p.crypto.eddsa.EdDSAPublicKey import java.security.* @@ -34,6 +35,7 @@ class X509EdDSAEngine : Signature { } override fun engineInitSign(privateKey: PrivateKey) = engine.initSign(privateKey) + @DeleteForDJVM override fun engineInitSign(privateKey: PrivateKey, random: SecureRandom) = engine.initSign(privateKey, random) override fun engineInitVerify(publicKey: PublicKey) { diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt index b6474bf769..540ddb5715 100644 --- a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt @@ -10,6 +10,7 @@ package net.corda.core.internal.cordapp +import net.corda.core.DeleteForDJVM import net.corda.core.cordapp.Cordapp import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic @@ -20,6 +21,7 @@ import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import java.net.URL +@DeleteForDJVM data class CordappImpl( override val contractClassNames: List, override val initiatedFlows: List>>, diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryService.kt b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryService.kt index 68041a760a..5b1c1b35af 100644 --- a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryService.kt +++ b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryService.kt @@ -1,5 +1,6 @@ package net.corda.core.internal.notary +import net.corda.core.DeleteForDJVM import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession import net.corda.core.flows.NotaryFlow @@ -8,6 +9,7 @@ import net.corda.core.node.ServiceHub import net.corda.core.serialization.SingletonSerializeAsToken import java.security.PublicKey +@DeleteForDJVM abstract class NotaryService : SingletonSerializeAsToken() { abstract val services: ServiceHub abstract val notaryIdentityKey: PublicKey diff --git a/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt index f0931bd94d..bfe34fdb24 100644 --- a/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt @@ -10,6 +10,7 @@ package net.corda.core.node +import net.corda.core.DeleteForDJVM import net.corda.core.flows.FlowLogic import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.FlowProgressHandle @@ -21,6 +22,7 @@ import rx.Observable * With the [AppServiceHub] parameter a [CordaService] is able to access to privileged operations. * In particular such a [CordaService] can initiate and track flows marked with [net.corda.core.flows.StartableByService]. */ +@DeleteForDJVM interface AppServiceHub : ServiceHub { /** diff --git a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt index 4e100475b0..7357834a77 100644 --- a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt +++ b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt @@ -10,6 +10,7 @@ package net.corda.core.node +import net.corda.core.KeepForDJVM import net.corda.core.identity.Party import net.corda.core.node.services.AttachmentId import net.corda.core.serialization.CordaSerializable @@ -33,6 +34,7 @@ import java.time.Instant * @property eventHorizon Time after which nodes will be removed from the network map if they have not been seen * during this period */ +@KeepForDJVM @CordaSerializable data class NetworkParameters( val minimumPlatformVersion: Int, @@ -110,5 +112,6 @@ data class NetworkParameters( * @property identity Identity of the notary (note that it can be an identity of the distributed node). * @property validating Indicates if the notary is validating. */ +@KeepForDJVM @CordaSerializable data class NotaryInfo(val identity: Party, val validating: Boolean) diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 4f517f924d..81855b35e9 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -10,6 +10,7 @@ package net.corda.core.node +import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement import net.corda.core.contracts.* import net.corda.core.cordapp.CordappContext @@ -32,6 +33,7 @@ import java.time.Clock * Subset of node services that are used for loading transactions from the wire into fully resolved, looked up * forms ready for verification. */ +@DeleteForDJVM @DoNotImplement interface ServicesForResolution { /** @@ -103,6 +105,7 @@ enum class StatesToRecord { * * In unit test environments, some of those services may be missing or mocked out. */ +@DeleteForDJVM interface ServiceHub : ServicesForResolution { // NOTE: Any services exposed to flows (public view) need to implement [SerializeAsToken] or similar to avoid // their internal state from being serialized in checkpoints. diff --git a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt index d1914410b3..1fb61cb34a 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt @@ -10,6 +10,7 @@ package net.corda.core.node.services +import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement import net.corda.core.concurrent.CordaFuture import net.corda.core.identity.AbstractParty @@ -68,7 +69,7 @@ interface NetworkMapCacheBase { /** Tracks changes to the network map cache. */ val changed: Observable /** Future to track completion of the NetworkMapService registration. */ - val nodeReady: CordaFuture + @get:DeleteForDJVM val nodeReady: CordaFuture /** * Atomically get the current party nodes and a stream of updates. Note that the Observable buffers updates until the diff --git a/core/src/main/kotlin/net/corda/core/node/services/TimeWindowChecker.kt b/core/src/main/kotlin/net/corda/core/node/services/TimeWindowChecker.kt index 9d8677d652..d4ecf8fab9 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/TimeWindowChecker.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/TimeWindowChecker.kt @@ -10,6 +10,7 @@ package net.corda.core.node.services +import net.corda.core.DeleteForDJVM import net.corda.core.contracts.TimeWindow import java.time.Clock @@ -17,6 +18,7 @@ import java.time.Clock * Checks if the current instant provided by the input clock falls within the provided time-window. */ @Deprecated("This class is no longer used") +@DeleteForDJVM class TimeWindowChecker(val clock: Clock = Clock.systemUTC()) { fun isValid(timeWindow: TimeWindow): Boolean = clock.instant() in timeWindow } diff --git a/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt index 40effb0f45..68ba19fcc6 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt @@ -10,6 +10,7 @@ package net.corda.core.node.services +import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.SecureHash @@ -20,6 +21,7 @@ import rx.Observable /** * Thread-safe storage of transactions. */ +@DeleteForDJVM @DoNotImplement interface TransactionStorage { /** diff --git a/core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt b/core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt index 6bb472e6c7..f7de22a5f9 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt @@ -10,6 +10,7 @@ package net.corda.core.node.services +import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement import net.corda.core.concurrent.CordaFuture import net.corda.core.transactions.LedgerTransaction @@ -19,6 +20,7 @@ import net.corda.core.transactions.LedgerTransaction * @suppress */ @DoNotImplement +@DeleteForDJVM interface TransactionVerifierService { /** * @param transaction The transaction to be verified. diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index abb5ddfe1f..12006608ae 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -11,6 +11,7 @@ package net.corda.core.node.services import co.paralleluniverse.fibers.Suspendable +import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.* @@ -187,6 +188,7 @@ interface VaultService { /** * Provide a [CordaFuture] for when a [StateRef] is consumed, which can be very useful in building tests. */ + @DeleteForDJVM fun whenConsumed(ref: StateRef): CordaFuture> { return updates.filter { it.consumed.any { it.ref == ref } }.toFuture() } diff --git a/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt b/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt index 8505acd989..fc4788a213 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt @@ -10,7 +10,7 @@ package net.corda.core.schemas -import com.google.common.base.CaseFormat +import net.corda.core.KeepForDJVM import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateRef import net.corda.core.serialization.CordaSerializable @@ -26,6 +26,7 @@ import javax.persistence.MappedSuperclass * A contract state that may be mapped to database schemas configured for this node to support querying for, * or filtering of, states. */ +@KeepForDJVM interface QueryableState : ContractState { /** * Enumerate the schemas this state can export representations of itself as. @@ -48,6 +49,7 @@ interface QueryableState : ContractState { * @param version The version number of this instance within the family. * @param mappedTypes The JPA entity classes that the ORM layer needs to be configure with for this schema. */ +@KeepForDJVM open class MappedSchema(schemaFamily: Class<*>, val version: Int, val mappedTypes: Iterable>) { @@ -88,6 +90,7 @@ open class MappedSchema(schemaFamily: Class<*>, * A super class for all mapped states exported to a schema that ensures the [StateRef] appears on the database row. The * [StateRef] will be set to the correct value by the framework (there's no need to set during mapping generation by the state itself). */ +@KeepForDJVM @MappedSuperclass @CordaSerializable class PersistentState(@EmbeddedId var stateRef: PersistentStateRef? = null) : StatePersistable @@ -95,6 +98,7 @@ class PersistentState(@EmbeddedId var stateRef: PersistentStateRef? = null) : St /** * Embedded [StateRef] representation used in state mapping. */ +@KeepForDJVM @Embeddable data class PersistentStateRef( @Column(name = "transaction_id", length = 64, nullable = false) @@ -109,8 +113,8 @@ data class PersistentStateRef( /** * Marker interface to denote a persistable Corda state entity that will always have a transaction id and index */ +@KeepForDJVM interface StatePersistable : Serializable - object MappedSchemaValidator { fun fieldsFromOtherMappedSchema(schema: MappedSchema) : List = schema.mappedTypes.map { entity -> diff --git a/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsException.kt b/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsException.kt index 43a63fa70e..3a0732a67b 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsException.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsException.kt @@ -11,8 +11,10 @@ package net.corda.core.serialization import net.corda.core.CordaException +import net.corda.core.KeepForDJVM import net.corda.core.crypto.SecureHash /** Thrown during deserialization to indicate that an attachment needed to construct the [WireTransaction] is not found. */ +@KeepForDJVM @CordaSerializable class MissingAttachmentsException(val ids: List) : CordaException() \ No newline at end of file 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 6f2f0e10bf..e3b85247d5 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt @@ -8,10 +8,13 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ +@file:KeepForDJVM package net.corda.core.serialization import net.corda.core.CordaInternal +import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement +import net.corda.core.KeepForDJVM import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 import net.corda.core.serialization.internal.effectiveSerializationEnv @@ -117,6 +120,7 @@ interface SerializationEncoding /** * Parameters to serialization and deserialization. */ +@KeepForDJVM @DoNotImplement interface SerializationContext { /** @@ -166,6 +170,7 @@ interface SerializationContext { /** * Helper method to return a new context based on this context with the deserialization class loader changed. */ + @DeleteForDJVM fun withClassLoader(classLoader: ClassLoader): SerializationContext /** @@ -193,6 +198,7 @@ interface SerializationContext { /** * The use case that we are serializing for, since it influences the implementations chosen. */ + @KeepForDJVM enum class UseCase { P2P, RPCServer, RPCClient, Storage, Checkpoint, Testing } } @@ -201,6 +207,7 @@ interface SerializationContext { * others being set that aren't keyed on this enumeration, but for general use properties adding a * well known key here is preferred. */ +@KeepForDJVM enum class ContextPropertyKeys { SERIALIZERS } @@ -208,13 +215,14 @@ enum class ContextPropertyKeys { /** * Global singletons to be used as defaults that are injected elsewhere (generally, in the node or in RPC client). */ +@KeepForDJVM object SerializationDefaults { val SERIALIZATION_FACTORY get() = effectiveSerializationEnv.serializationFactory val P2P_CONTEXT get() = effectiveSerializationEnv.p2pContext - val RPC_SERVER_CONTEXT get() = effectiveSerializationEnv.rpcServerContext - val RPC_CLIENT_CONTEXT get() = effectiveSerializationEnv.rpcClientContext - val STORAGE_CONTEXT get() = effectiveSerializationEnv.storageContext - val CHECKPOINT_CONTEXT get() = effectiveSerializationEnv.checkpointContext + @DeleteForDJVM val RPC_SERVER_CONTEXT get() = effectiveSerializationEnv.rpcServerContext + @DeleteForDJVM val RPC_CLIENT_CONTEXT get() = effectiveSerializationEnv.rpcClientContext + @DeleteForDJVM val STORAGE_CONTEXT get() = effectiveSerializationEnv.storageContext + @DeleteForDJVM val CHECKPOINT_CONTEXT get() = effectiveSerializationEnv.checkpointContext } /** @@ -254,6 +262,7 @@ inline fun ByteArray.deserialize(serializationFactory: Seriali /** * Convenience extension method for deserializing a JDBC Blob, utilising the defaults. */ +@DeleteForDJVM inline fun Blob.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): T { return this.getBytes(1, this.length().toInt()).deserialize(serializationFactory, context) @@ -272,6 +281,7 @@ fun T.serialize(serializationFactory: SerializationFactory = Serializa * to get the original object back. */ @Suppress("unused") +@KeepForDJVM class SerializedBytes(bytes: ByteArray) : OpaqueBytes(bytes) { companion object { /** @@ -295,10 +305,12 @@ class SerializedBytes(bytes: ByteArray) : OpaqueBytes(bytes) { val hash: SecureHash by lazy { bytes.sha256() } } +@KeepForDJVM interface ClassWhitelist { fun hasListed(type: Class<*>): Boolean } +@KeepForDJVM @DoNotImplement interface EncodingWhitelist { fun acceptEncoding(encoding: SerializationEncoding): Boolean diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomSerializer.kt index b2b1ce6236..385e71dafc 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomSerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomSerializer.kt @@ -10,6 +10,8 @@ package net.corda.core.serialization +import net.corda.core.KeepForDJVM + /** * Allows CorDapps to provide custom serializers for third party libraries where those libraries cannot * be recompiled with the -parameters flag rendering their classes natively serializable by Corda. In this case @@ -19,6 +21,7 @@ package net.corda.core.serialization * NOTE: The proxy object should be specified as a separate class. However, this can be defined within the * scope of the custom serializer. */ +@KeepForDJVM interface SerializationCustomSerializer { /** * Should facilitate the conversion of the third party object into the serializable diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationToken.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationToken.kt index ef44b9e16d..2f16eae17c 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationToken.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationToken.kt @@ -10,6 +10,7 @@ package net.corda.core.serialization +import net.corda.core.DeleteForDJVM import net.corda.core.node.ServiceHub import net.corda.core.serialization.SingletonSerializationToken.Companion.singletonSerializationToken @@ -28,6 +29,7 @@ import net.corda.core.serialization.SingletonSerializationToken.Companion.single * * This models a similar pattern to the readReplace/writeReplace methods in Java serialization. */ +@DeleteForDJVM @CordaSerializable interface SerializeAsToken { fun toToken(context: SerializeAsTokenContext): SerializationToken @@ -36,6 +38,7 @@ interface SerializeAsToken { /** * This represents a token in the serialized stream for an instance of a type that implements [SerializeAsToken]. */ +@DeleteForDJVM interface SerializationToken { fun fromToken(context: SerializeAsTokenContext): Any } @@ -43,6 +46,7 @@ interface SerializationToken { /** * A context for mapping SerializationTokens to/from SerializeAsTokens. */ +@DeleteForDJVM interface SerializeAsTokenContext { val serviceHub: ServiceHub fun putSingleton(toBeTokenized: SerializeAsToken) @@ -53,6 +57,7 @@ interface SerializeAsTokenContext { * A class representing a [SerializationToken] for some object that is not serializable but can be looked up * (when deserialized) via just the class name. */ +@DeleteForDJVM class SingletonSerializationToken private constructor(private val className: String) : SerializationToken { override fun fromToken(context: SerializeAsTokenContext) = context.getSingleton(className) @@ -68,6 +73,7 @@ class SingletonSerializationToken private constructor(private val className: Str * A base class for implementing large objects / components / services that need to serialize themselves to a string token * to indicate which instance the token is a serialized form of. */ +@DeleteForDJVM abstract class SingletonSerializeAsToken : SerializeAsToken { private val token = singletonSerializationToken(javaClass) diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationWhitelist.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationWhitelist.kt index b9e6c4c988..ed6db13384 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationWhitelist.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationWhitelist.kt @@ -10,11 +10,14 @@ package net.corda.core.serialization +import net.corda.core.KeepForDJVM + /** * Provide a subclass of this via the [java.util.ServiceLoader] mechanism to be able to whitelist types for * serialisation that you cannot otherwise annotate. The name of the class must appear in a text file on the * classpath under the path META-INF/services/net.corda.core.serialization.SerializationWhitelist */ +@KeepForDJVM interface SerializationWhitelist { /** * Optionally whitelist types for use in object serialization, as we lock down the types that can be serialized. diff --git a/core/src/main/kotlin/net/corda/core/serialization/internal/SerializationEnvironment.kt b/core/src/main/kotlin/net/corda/core/serialization/internal/SerializationEnvironment.kt index 94b061c954..adfcbf8bc4 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/internal/SerializationEnvironment.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/internal/SerializationEnvironment.kt @@ -8,8 +8,10 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ +@file:KeepForDJVM package net.corda.core.serialization.internal +import net.corda.core.KeepForDJVM import net.corda.core.internal.InheritableThreadLocalToggleField import net.corda.core.internal.SimpleToggleField import net.corda.core.internal.ThreadLocalToggleField @@ -17,6 +19,7 @@ import net.corda.core.internal.VisibleForTesting import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationFactory +@KeepForDJVM interface SerializationEnvironment { val serializationFactory: SerializationFactory val p2pContext: SerializationContext @@ -26,6 +29,7 @@ interface SerializationEnvironment { val checkpointContext: SerializationContext } +@KeepForDJVM open class SerializationEnvironmentImpl( override val serializationFactory: SerializationFactory, override val p2pContext: SerializationContext, diff --git a/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt index 04923df4d8..fa1e568cf1 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt @@ -11,6 +11,7 @@ package net.corda.core.transactions import net.corda.core.DoNotImplement +import net.corda.core.KeepForDJVM import net.corda.core.contracts.* import net.corda.core.identity.Party import net.corda.core.internal.castIfPossible @@ -21,6 +22,7 @@ import java.util.function.Predicate /** * An abstract class defining fields shared by all transaction types in the system. */ +@KeepForDJVM @DoNotImplement abstract class BaseTransaction : NamedByHash { /** The inputs of this transaction. Note that in BaseTransaction subclasses the type of this list may change! */ diff --git a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt index bf7e8ca9f9..dc98b51b7c 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt @@ -10,6 +10,7 @@ package net.corda.core.transactions +import net.corda.core.KeepForDJVM import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature @@ -32,6 +33,7 @@ import java.security.PublicKey // TODO: check transaction size is within limits /** A special transaction for upgrading the contract of a state. */ +@KeepForDJVM @CordaSerializable data class ContractUpgradeWireTransaction( /** @@ -120,6 +122,7 @@ data class ContractUpgradeWireTransaction( * is no flexibility on what parts of the transaction to reveal – the inputs and notary field are always visible and the * rest of the transaction is always hidden. Its only purpose is to hide transaction data when using a non-validating notary. */ +@KeepForDJVM @CordaSerializable data class ContractUpgradeFilteredTransaction( /** Transaction components that are exposed. */ @@ -168,6 +171,7 @@ data class ContractUpgradeFilteredTransaction( * In contrast with a regular transaction, signatures are checked against the signers specified by input states' * *participants* fields, so full resolution is needed for signature verification. */ +@KeepForDJVM data class ContractUpgradeLedgerTransaction( override val inputs: List>, override val notary: Party, diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 53bee551b7..aba11c519b 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -10,6 +10,7 @@ package net.corda.core.transactions +import net.corda.core.KeepForDJVM import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party @@ -36,6 +37,7 @@ import java.util.function.Predicate // TODO LedgerTransaction is not supposed to be serialisable as it references attachments, etc. The verification logic // currently sends this across to out-of-process verifiers. We'll need to change that first. // DOCSTART 1 +@KeepForDJVM @CordaSerializable data class LedgerTransaction @JvmOverloads constructor( /** The resolved input states which will be consumed/invalidated by the execution of this transaction. */ @@ -68,10 +70,14 @@ data class LedgerTransaction @JvmOverloads constructor( .asSubclass(Contract::class.java) } } + + private fun stateToContractClass(state: TransactionState): Try> { + return contractClassFor(state.contract, state.data::class.java.classLoader) + } } private val contracts: Map>> = (inputs.map { it.state } + outputs) - .map { it.contract to contractClassFor(it.contract, it.data::class.java.classLoader) }.toMap() + .map { it.contract to stateToContractClass(it) }.toMap() val inputStates: List get() = inputs.map { it.state.data } @@ -247,6 +253,7 @@ data class LedgerTransaction @JvmOverloads constructor( * be used to simplify this logic. */ // DOCSTART 3 + @KeepForDJVM data class InOutGroup(val inputs: List, val outputs: List, val groupingKey: K) // DOCEND 3 diff --git a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt index 581601b901..07584a0f3e 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt @@ -11,6 +11,7 @@ package net.corda.core.transactions import net.corda.core.CordaException +import net.corda.core.KeepForDJVM import net.corda.core.contracts.* import net.corda.core.crypto.* import net.corda.core.identity.Party @@ -117,6 +118,7 @@ abstract class TraversableTransaction(open val componentGroups: List, val nonces: List, val partialMerkleTree: PartialMerkleTree) : ComponentGroup(groupIndex, components) { init { @@ -352,6 +355,7 @@ data class FilteredComponentGroup(override val groupIndex: Int, override val com * @param id transaction's id. * @param reason information about the exception. */ +@KeepForDJVM @CordaSerializable class ComponentVisibilityException(val id: SecureHash, val reason: String) : CordaException("Component visibility error for transaction with id:$id. Reason: $reason") @@ -359,5 +363,6 @@ class ComponentVisibilityException(val id: SecureHash, val reason: String) : Cor * @param id transaction's id. * @param reason information about the exception. */ +@KeepForDJVM @CordaSerializable class FilteredTransactionVerificationException(val id: SecureHash, val reason: String) : CordaException("Transaction with id:$id cannot be verified. Reason: $reason") diff --git a/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt b/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt index 433ee3df9b..f7ca56aad7 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt @@ -10,6 +10,7 @@ package net.corda.core.transactions +import net.corda.core.KeepForDJVM import net.corda.core.contracts.ContractState import net.corda.core.contracts.TransactionState import net.corda.core.flows.FlowException @@ -21,6 +22,7 @@ import net.corda.core.serialization.CordaSerializable * @property states States which have contracts that do not have corresponding attachments in the attachment store. */ @CordaSerializable +@KeepForDJVM class MissingContractAttachments(val states: List>) : FlowException("Cannot find contract attachments for ${states.map { it.contract }.distinct()}. " + "See https://docs.corda.net/api-contract-constraints.html#debugging") \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt index c18175531d..86e95881b9 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt @@ -10,6 +10,8 @@ package net.corda.core.transactions +import net.corda.core.DeleteForDJVM +import net.corda.core.KeepForDJVM import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature @@ -31,6 +33,7 @@ import java.security.PublicKey * on the fly. */ @CordaSerializable +@KeepForDJVM data class NotaryChangeWireTransaction( /** * Contains all of the transaction components in serialized form. @@ -71,12 +74,14 @@ data class NotaryChangeWireTransaction( } /** Resolves input states and builds a [NotaryChangeLedgerTransaction]. */ + @DeleteForDJVM fun resolve(services: ServicesForResolution, sigs: List): NotaryChangeLedgerTransaction { val resolvedInputs = services.loadStates(inputs.toSet()).toList() return NotaryChangeLedgerTransaction(resolvedInputs, notary, newNotary, id, sigs) } /** Resolves input states and builds a [NotaryChangeLedgerTransaction]. */ + @DeleteForDJVM fun resolve(services: ServiceHub, sigs: List) = resolve(services as ServicesForResolution, sigs) enum class Component { @@ -92,6 +97,7 @@ data class NotaryChangeWireTransaction( * signatures are checked against the signers specified by input states' *participants* fields, so full resolution is * needed for signature verification. */ +@KeepForDJVM data class NotaryChangeLedgerTransaction( override val inputs: List>, override val notary: Party, diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index 3cc7cd2c5c..d5a5dd97cf 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -12,6 +12,8 @@ package net.corda.core.transactions import net.corda.core.CordaException import net.corda.core.CordaThrowable +import net.corda.core.DeleteForDJVM +import net.corda.core.KeepForDJVM import net.corda.core.contracts.* import net.corda.core.crypto.* import net.corda.core.identity.Party @@ -43,6 +45,7 @@ import java.util.function.Predicate * sign. */ // DOCSTART 1 +@KeepForDJVM @CordaSerializable data class SignedTransaction(val txBits: SerializedBytes, override val sigs: List @@ -141,6 +144,7 @@ data class SignedTransaction(val txBits: SerializedBytes, * @throws SignaturesMissingException if any signatures that should have been present are missing. */ @JvmOverloads + @DeleteForDJVM @Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class) fun toLedgerTransaction(services: ServiceHub, checkSufficientSignatures: Boolean = true): LedgerTransaction { // TODO: We could probably optimise the below by @@ -169,6 +173,7 @@ data class SignedTransaction(val txBits: SerializedBytes, * @throws SignaturesMissingException if any signatures that should have been present are missing. */ @JvmOverloads + @DeleteForDJVM @Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class) fun verify(services: ServiceHub, checkSufficientSignatures: Boolean = true) { when (coreTransaction) { @@ -179,6 +184,7 @@ data class SignedTransaction(val txBits: SerializedBytes, } /** No contract code is run when verifying notary change transactions, it is sufficient to check invariants during initialisation. */ + @DeleteForDJVM private fun verifyNotaryChangeTransaction(services: ServiceHub, checkSufficientSignatures: Boolean) { val ntx = resolveNotaryChangeTransaction(services) if (checkSufficientSignatures) ntx.verifyRequiredSignatures() @@ -186,6 +192,7 @@ data class SignedTransaction(val txBits: SerializedBytes, } /** No contract code is run when verifying contract upgrade transactions, it is sufficient to check invariants during initialisation. */ + @DeleteForDJVM private fun verifyContractUpgradeTransaction(services: ServicesForResolution, checkSufficientSignatures: Boolean) { val ctx = resolveContractUpgradeTransaction(services) if (checkSufficientSignatures) ctx.verifyRequiredSignatures() @@ -195,6 +202,7 @@ data class SignedTransaction(val txBits: SerializedBytes, // TODO: Verify contract constraints here as well as in LedgerTransaction to ensure that anything being deserialised // from the attachment is trusted. This will require some partial serialisation work to not load the ContractState // objects from the TransactionState. + @DeleteForDJVM private fun verifyRegularTransaction(services: ServiceHub, checkSufficientSignatures: Boolean) { val ltx = toLedgerTransaction(services, checkSufficientSignatures) // TODO: allow non-blocking verification. @@ -205,6 +213,7 @@ data class SignedTransaction(val txBits: SerializedBytes, * Resolves the underlying base transaction and then returns it, handling any special case transactions such as * [NotaryChangeWireTransaction]. */ + @DeleteForDJVM fun resolveBaseTransaction(servicesForResolution: ServicesForResolution): BaseTransaction { return when (coreTransaction) { is NotaryChangeWireTransaction -> resolveNotaryChangeTransaction(servicesForResolution) @@ -220,6 +229,7 @@ data class SignedTransaction(val txBits: SerializedBytes, * Resolves the underlying transaction with signatures and then returns it, handling any special case transactions * such as [NotaryChangeWireTransaction]. */ + @DeleteForDJVM fun resolveTransactionWithSignatures(services: ServicesForResolution): TransactionWithSignatures { return when (coreTransaction) { is NotaryChangeWireTransaction -> resolveNotaryChangeTransaction(services) @@ -234,6 +244,7 @@ data class SignedTransaction(val txBits: SerializedBytes, * If [transaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a * [NotaryChangeLedgerTransaction] so the signatures can be verified. */ + @DeleteForDJVM fun resolveNotaryChangeTransaction(services: ServicesForResolution): NotaryChangeLedgerTransaction { val ntx = coreTransaction as? NotaryChangeWireTransaction ?: throw IllegalStateException("Expected a ${NotaryChangeWireTransaction::class.simpleName} but found ${coreTransaction::class.simpleName}") @@ -244,12 +255,14 @@ data class SignedTransaction(val txBits: SerializedBytes, * If [transaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a * [NotaryChangeLedgerTransaction] so the signatures can be verified. */ + @DeleteForDJVM fun resolveNotaryChangeTransaction(services: ServiceHub) = resolveNotaryChangeTransaction(services as ServicesForResolution) /** * If [coreTransaction] is a [ContractUpgradeWireTransaction], loads the input states and resolves it to a * [ContractUpgradeLedgerTransaction] so the signatures can be verified. */ + @DeleteForDJVM fun resolveContractUpgradeTransaction(services: ServicesForResolution): ContractUpgradeLedgerTransaction { val ctx = coreTransaction as? ContractUpgradeWireTransaction ?: throw IllegalStateException("Expected a ${ContractUpgradeWireTransaction::class.simpleName} but found ${coreTransaction::class.simpleName}") @@ -263,6 +276,7 @@ data class SignedTransaction(val txBits: SerializedBytes, "Missing signatures for $descriptions on transaction ${id.prefixChars()} for ${missing.joinToString()}" } + @KeepForDJVM @CordaSerializable class SignaturesMissingException(val missing: Set, val descriptions: List, override val id: SecureHash) : NamedByHash, SignatureException(missingSignatureMsg(missing, descriptions, id)), CordaThrowable by CordaException(missingSignatureMsg(missing, descriptions, id)) diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 8000a3aaa6..80ed0fb27f 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -11,6 +11,7 @@ package net.corda.core.transactions import co.paralleluniverse.strands.Strand +import net.corda.core.DeleteForDJVM import net.corda.core.contracts.* import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.SecureHash @@ -43,6 +44,7 @@ import kotlin.collections.ArrayList * When this is set to a non-null value, an output state can be added by just passing in a [ContractState] – a * [TransactionState] with this notary specified will be generated automatically. */ +@DeleteForDJVM open class TransactionBuilder( var notary: Party? = null, var lockId: UUID = (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID(), diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt index 00435c3d5e..621df96f91 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt @@ -11,6 +11,7 @@ package net.corda.core.transactions import net.corda.core.DoNotImplement +import net.corda.core.KeepForDJVM import net.corda.core.contracts.NamedByHash import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.isFulfilledBy @@ -22,6 +23,7 @@ import java.security.SignatureException import java.util.* /** An interface for transactions containing signatures, with logic for signature verification. */ +@KeepForDJVM @DoNotImplement interface TransactionWithSignatures : NamedByHash { /** diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt index 138522b07b..202eb21278 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -11,6 +11,8 @@ package net.corda.core.transactions import net.corda.core.CordaInternal +import net.corda.core.DeleteForDJVM +import net.corda.core.KeepForDJVM import net.corda.core.contracts.* import net.corda.core.contracts.ComponentGroupEnum.* import net.corda.core.crypto.* @@ -51,9 +53,13 @@ import java.util.function.Predicate *

    */ @CordaSerializable -class WireTransaction(componentGroups: List, val privacySalt: PrivacySalt = PrivacySalt()) : TraversableTransaction(componentGroups) { +@KeepForDJVM +class WireTransaction(componentGroups: List, val privacySalt: PrivacySalt) : TraversableTransaction(componentGroups) { + @DeleteForDJVM + constructor(componentGroups: List) : this(componentGroups, PrivacySalt()) @Deprecated("Required only in some unit-tests and for backwards compatibility purposes.", ReplaceWith("WireTransaction(val componentGroups: List, override val privacySalt: PrivacySalt)"), DeprecationLevel.WARNING) + @DeleteForDJVM constructor(inputs: List, attachments: List, outputs: List>, @@ -95,6 +101,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr * @throws TransactionResolutionException if an input points to a transaction not found in storage. */ @Throws(AttachmentResolutionException::class, TransactionResolutionException::class) + @DeleteForDJVM fun toLedgerTransaction(services: ServicesForResolution): LedgerTransaction { return toLedgerTransactionInternal( resolveIdentity = { services.identityService.partyFromKey(it) }, @@ -264,6 +271,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr } } + @DeleteForDJVM override fun toString(): String { val buf = StringBuilder() buf.appendln("Transaction:") diff --git a/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt index 344e74affc..eff9cd748d 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt @@ -9,9 +9,11 @@ */ @file:JvmName("ByteArrays") +@file:KeepForDJVM package net.corda.core.utilities +import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable import java.io.ByteArrayInputStream import java.io.OutputStream @@ -29,6 +31,7 @@ import javax.xml.bind.DatatypeConverter * @property size The number of bytes this sequence represents. */ @CordaSerializable +@KeepForDJVM sealed class ByteSequence(private val _bytes: ByteArray, val offset: Int, val size: Int) : Comparable { /** * The underlying bytes. Some implementations may choose to make a copy of the underlying [ByteArray] for @@ -154,6 +157,7 @@ sealed class ByteSequence(private val _bytes: ByteArray, val offset: Int, val si * In an ideal JVM this would be a value type and be completely overhead free. Project Valhalla is adding such * functionality to Java, but it won't arrive for a few years yet! */ +@KeepForDJVM open class OpaqueBytes(bytes: ByteArray) : ByteSequence(bytes, 0, bytes.size) { companion object { /** @@ -199,6 +203,7 @@ fun String.parseAsHex(): ByteArray = DatatypeConverter.parseHexBinary(this) /** * Class is public for serialization purposes */ +@KeepForDJVM class OpaqueBytesSubSequence(override val bytes: ByteArray, offset: Int, size: Int) : ByteSequence(bytes, offset, size) { init { require(offset >= 0 && offset < bytes.size) diff --git a/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt index e303e54b66..936d4c0800 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt @@ -9,9 +9,11 @@ */ @file:JvmName("EncodingUtils") +@file:KeepForDJVM package net.corda.core.utilities +import net.corda.core.KeepForDJVM import net.corda.core.crypto.Base58 import net.corda.core.crypto.Crypto import net.corda.core.internal.hash diff --git a/core/src/main/kotlin/net/corda/core/utilities/Id.kt b/core/src/main/kotlin/net/corda/core/utilities/Id.kt index 3b3df2f85d..961b8dbd2e 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/Id.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/Id.kt @@ -10,6 +10,7 @@ package net.corda.core.utilities +import net.corda.core.DeleteForDJVM import java.time.Instant import java.time.Instant.now @@ -25,6 +26,7 @@ open class Id(val value: VALUE, val entityType: String?, val ti /** * Creates an id using [Instant.now] as timestamp. */ + @DeleteForDJVM @JvmStatic fun newInstance(value: V, entityType: String? = null, timestamp: Instant = now()) = Id(value, entityType, timestamp) } diff --git a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt index 1300e2cf9c..1f31a5e384 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt @@ -8,8 +8,11 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ +@file:KeepForDJVM package net.corda.core.utilities +import net.corda.core.DeleteForDJVM +import net.corda.core.KeepForDJVM import net.corda.core.internal.concurrent.get import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.CordaSerializable @@ -135,6 +138,7 @@ private class TransientProperty internal constructor(private val initiali fun Collection.toNonEmptySet(): NonEmptySet = NonEmptySet.copyOf(this) /** Same as [Future.get] except that the [ExecutionException] is unwrapped. */ +@DeleteForDJVM fun Future.getOrThrow(timeout: Duration? = null): V = try { get(timeout) } catch (e: ExecutionException) { diff --git a/core/src/main/kotlin/net/corda/core/utilities/NonEmptySet.kt b/core/src/main/kotlin/net/corda/core/utilities/NonEmptySet.kt index 142480829c..1c089bf4c7 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/NonEmptySet.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/NonEmptySet.kt @@ -10,6 +10,7 @@ package net.corda.core.utilities +import net.corda.core.KeepForDJVM import java.util.* import java.util.function.Consumer import java.util.stream.Stream @@ -17,6 +18,7 @@ import java.util.stream.Stream /** * An immutable ordered non-empty set. */ +@KeepForDJVM class NonEmptySet private constructor(private val elements: Set) : Set by elements { companion object { /** diff --git a/core/src/main/kotlin/net/corda/core/utilities/Try.kt b/core/src/main/kotlin/net/corda/core/utilities/Try.kt index c6409664f6..686c7e228d 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/Try.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/Try.kt @@ -10,6 +10,7 @@ package net.corda.core.utilities +import net.corda.core.KeepForDJVM import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.Try.Failure @@ -69,6 +70,7 @@ sealed class Try { is Failure -> uncheckedCast(this) } + @KeepForDJVM data class Success(val value: A) : Try
    () { override val isSuccess: Boolean get() = true override val isFailure: Boolean get() = false @@ -76,6 +78,7 @@ sealed class Try { override fun toString(): String = "Success($value)" } + @KeepForDJVM data class Failure(val exception: Throwable) : Try() { override val isSuccess: Boolean get() = false override val isFailure: Boolean get() = true diff --git a/core/src/main/kotlin/net/corda/core/utilities/UntrustworthyData.kt b/core/src/main/kotlin/net/corda/core/utilities/UntrustworthyData.kt index 57a81d9bec..0f4fabb103 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/UntrustworthyData.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/UntrustworthyData.kt @@ -8,13 +8,12 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ +@file:KeepForDJVM package net.corda.core.utilities import co.paralleluniverse.fibers.Suspendable +import net.corda.core.KeepForDJVM import net.corda.core.flows.FlowException -import net.corda.core.internal.castIfPossible -import net.corda.core.serialization.SerializationDefaults -import net.corda.core.serialization.SerializedBytes import java.io.Serializable /** @@ -28,11 +27,13 @@ import java.io.Serializable * - Are any objects *reachable* from this object mismatched or not what you expected? * - Is it suspiciously large or small? */ +@KeepForDJVM class UntrustworthyData(@PublishedApi internal val fromUntrustedWorld: T) { @Suspendable @Throws(FlowException::class) fun unwrap(validator: Validator) = validator.validate(fromUntrustedWorld) + @KeepForDJVM @FunctionalInterface interface Validator : Serializable { @Suspendable diff --git a/core/src/main/kotlin/net/corda/core/utilities/UuidGenerator.kt b/core/src/main/kotlin/net/corda/core/utilities/UuidGenerator.kt index c024e3dbca..6c3f793a5d 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/UuidGenerator.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/UuidGenerator.kt @@ -10,8 +10,10 @@ package net.corda.core.utilities +import net.corda.core.DeleteForDJVM import java.util.* +@DeleteForDJVM class UuidGenerator { companion object { diff --git a/create-jdk8u/.gitignore b/create-jdk8u/.gitignore new file mode 100644 index 0000000000..c5a9347ba7 --- /dev/null +++ b/create-jdk8u/.gitignore @@ -0,0 +1,2 @@ +jdk8u/ +libs/ diff --git a/create-jdk8u/Makefile b/create-jdk8u/Makefile new file mode 100644 index 0000000000..424d073e41 --- /dev/null +++ b/create-jdk8u/Makefile @@ -0,0 +1,25 @@ +.DEFAULT_GOAL=all + +jdk8u: + git clone -b deterministic-jvm8 --single-branch https://github.com/corda/openjdk $@ + +jdk8u/common/autoconf/configure: jdk8u + +jdk8u/build/%/spec.gmk: jdk8u/common/autoconf/configure + cd jdk8u && $(SHELL) configure + +.PHONY: jdk-image clean all +jdk-image: jdk8u/build/linux-x86_64-normal-server-release/spec.gmk + $(MAKE) -C jdk8u images docs + +all: libs/rt.jar libs/jce.jar libs/jsse.jar + +clean: + $(MAKE) -C jdk8u clean + +libs: + mkdir $@ + +libs/rt.jar libs/jce.jar libs/jsse.jar: libs jdk-image + cp -f jdk8u/build/*/images/j2re-image/lib/$(@F) $@ + diff --git a/create-jdk8u/build.gradle b/create-jdk8u/build.gradle new file mode 100644 index 0000000000..db71504863 --- /dev/null +++ b/create-jdk8u/build.gradle @@ -0,0 +1,168 @@ +/* + * R3 Proprietary and Confidential + * + * Copyright (c) 2018 R3 Limited. All rights reserved. + * + * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. + * + * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. + */ +buildscript { + Properties constants = new Properties() + file("../constants.properties").withInputStream { constants.load(it) } + + ext { + artifactory_contextUrl = 'https://ci-artifactory.corda.r3cev.com/artifactory' + artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion') + proguard_version = constants.getProperty("proguardVersion") + } + + repositories { + mavenLocal() + mavenCentral() + jcenter() + maven { + url "$artifactory_contextUrl/corda-dev" + } + } + dependencies { + classpath "org.jfrog.buildinfo:build-info-extractor-gradle:$artifactory_plugin_version" + classpath "net.sf.proguard:proguard-gradle:$proguard_version" + } +} + +plugins { + id 'base' + id 'maven-publish' +} +apply plugin: 'com.jfrog.artifactory' + +/* + * This is a nested and independent Gradle project, + * and so has its own group and version. + * + * NOTE: The deterministic APIs are Open Source. + */ +group 'net.corda' +version '1.0-SNAPSHOT' + +task cleanJdk(type: Exec) { + commandLine 'make', 'clean' +} + +task makeJdk(type: Exec) { + // See: https://github.com/corda/openjdk/tree/deterministic-jvm8 + commandLine 'make' +} + +task runtimeJar(type: Jar, dependsOn: makeJdk) { + baseName 'deterministic-rt' + + from(zipTree("libs/rt.jar")) + from(zipTree("libs/jce.jar")) + from(zipTree("libs/jsse.jar")) + + reproducibleFileOrder = true + includeEmptyDirs = false +} + +import proguard.gradle.ProGuardTask +task validate(type: ProGuardTask) { + injars runtimeJar + + dontwarn 'java.lang.invoke.**' + dontwarn 'javax.lang.model.**' + dontwarn 'jdk.Exported' + + keepattributes '*' + dontpreverify + dontobfuscate + dontoptimize + verbose + + keep 'class *' +} +runtimeJar.finalizedBy validate + +task apiJar(type: Jar, dependsOn: runtimeJar) { + baseName 'deterministic-rt' + classifier 'api' + + from(zipTree(runtimeJar.outputs.files.singleFile)) { + include 'java/' + include 'javax/' + exclude 'java/awt/' + exclude 'java/beans/Weak*.class' + exclude 'java/lang/invoke/' + exclude 'java/lang/*Thread*.class' + exclude 'java/lang/Shutdown*.class' + exclude 'java/lang/ref/' + exclude 'java/lang/reflect/InvocationHandler.class' + exclude 'java/lang/reflect/Proxy*.class' + exclude 'java/lang/reflect/Weak*.class' + exclude 'java/io/File.class' + exclude 'java/io/File$*.class' + exclude 'java/io/*FileSystem.class' + exclude 'java/io/Filename*.class' + exclude 'java/io/FileDescriptor*.class' + exclude 'java/io/FileFilter*.class' + exclude 'java/io/FilePermission*.class' + exclude 'java/io/FileReader*.class' + exclude 'java/io/FileSystem*.class' + exclude 'java/io/File*Stream*.class' + exclude 'java/net/*Content*.class' + exclude 'java/net/Host*.class' + exclude 'java/net/Inet*.class' + exclude 'java/nio/file/Path.class' + exclude 'java/nio/file/attribute/' + exclude 'java/util/SplittableRandom*.class' + exclude 'java/util/Random.class' + exclude 'java/util/Random$*.class' + exclude 'java/util/WeakHashMap*.class' + exclude 'java/util/concurrent/*.class' + exclude 'java/util/concurrent/locks/' + exclude 'javax/activation/' + } + + preserveFileTimestamps = false + reproducibleFileOrder = true + includeEmptyDirs = false +} + +defaultTasks "build" +assemble.dependsOn runtimeJar +assemble.dependsOn apiJar +clean.dependsOn cleanJdk + +artifacts { + archives runtimeJar + archives apiJar +} + +artifactory { + contextUrl = artifactory_contextUrl + publish { + repository { + repoKey = 'corda-dev' + username = System.getenv('CORDA_ARTIFACTORY_USERNAME') + password = System.getenv('CORDA_ARTIFACTORY_PASSWORD') + maven = true + } + + defaults { + publications('mavenJava') + } + } +} + +publishing { + publications { + mavenJava(MavenPublication) { + artifactId 'deterministic-rt' + artifact runtimeJar + artifact apiJar + } + } +} + +task install(dependsOn: publishToMavenLocal) diff --git a/create-jdk8u/settings.gradle b/create-jdk8u/settings.gradle new file mode 100644 index 0000000000..0a4db38e70 --- /dev/null +++ b/create-jdk8u/settings.gradle @@ -0,0 +1,10 @@ +/* + * R3 Proprietary and Confidential + * + * Copyright (c) 2018 R3 Limited. All rights reserved. + * + * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. + * + * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. + */ +rootProject.name = 'deterministic-rt' diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 482fd455ba..fa47f114ae 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -8,6 +8,9 @@ Unreleased ========== * Introduced a hierarchy of ``DatabaseMigrationException``s, allowing ``NodeStartup`` to gracefully inform users of problems related to database migrations before exiting with a non-zero code. +* Fixed an issue where ``trackBy`` was returning ``ContractStates`` from a transaction that were not being tracked. The + unrelated ``ContractStates`` will now be filtered out from the returned ``Vault.Update``. + * Introducing the flow hospital - a component of the node that manages flows that have errored and whether they should be retried from their previous checkpoints or have their errors propagate. Currently it will respond to any error that occurs during the resolution of a received transaction as part of ``FinalityFlow``. In such a scenerio the receiving @@ -34,6 +37,10 @@ Unreleased * Improved audit trail for ``FinalityFlow`` and related sub-flows. +* Notary client flow retry logic was improved to handle validating flows better. Instead of re-sending flow messages the + entire flow is now restarted after a timeout. The relevant node configuration section was renamed from ``p2pMessagingRetry``, + to ``flowTimeout`` to reflect the behaviour change. + * The node's configuration is only printed on startup if ``devMode`` is ``true``, avoiding the risk of printing passwords in a production setup. diff --git a/docs/source/design/sgx-infrastructure/Example SGX deployment.png b/docs/source/design/sgx-infrastructure/Example SGX deployment.png new file mode 100644 index 0000000000..1ccdcddc65 Binary files /dev/null and b/docs/source/design/sgx-infrastructure/Example SGX deployment.png differ diff --git a/docs/source/design/sgx-infrastructure/decisions/certification.md b/docs/source/design/sgx-infrastructure/decisions/certification.md new file mode 100644 index 0000000000..afd7e05718 --- /dev/null +++ b/docs/source/design/sgx-infrastructure/decisions/certification.md @@ -0,0 +1,69 @@ +![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png) + +-------------------------------------------- +Design Decision: CPU certification method +============================================ + +## Background / Context + +Remote attestation is done in two main steps. +1. Certification of the CPU. This boils down to some kind of Intel signature over a key that only a specific enclave has + access to. +2. Using the certified key to sign business logic specific enclave quotes and providing the full chain of trust to + challengers. + +This design question concerns the way we can manage a certification key. A more detailed description is +[here](../details/attestation.md) + +## Options Analysis + +### A. Use Intel's recommended protocol + +This involves using aesmd and the Intel SDK to establish an opaque attestation key that transparently signs quotes. +Then for each enclave we need to do several roundtrips to IAS to get a revocation list (which we don't need) and request +a direct Intel signature over the quote (which we shouldn't need as the trust has been established already during EPID +join) + +#### Advantages + +1. We have a PoC implemented that does this + +#### Disadvantages + +1. Frequent roundtrips to Intel infrastructure +2. Intel can reproduce the certifying private key +3. Involves unnecessary protocol steps and features we don't need (EPID) + +### B. Use Intel's protocol to bootstrap our own certificate + +This involves using Intel's current attestation protocol to have Intel sign over our own certifying enclave's +certificate that derives its certification key using the sealing fuse values. + +#### Advantages + +1. Certifying key not reproducible by Intel +2. Allows for our own CPU enrollment process, should we need one +3. Infrequent roundtrips to Intel infrastructure (only needed once per microcode update) + +#### Disadvantages + +1. Still uses the EPID protocol + +### C. Intercept Intel's recommended protocol + +This involves using Intel's current protocol as is but instead of doing roundtrips to IAS to get signatures over quotes +we try to establish the chain of trust during EPID provisioning and reuse it later. + +#### Advantages + +1. Uses Intel's current protocol +2. Infrequent rountrips to Intel infrastructure + +#### Disadvantages + +1. The provisioning protocol is underdocumented and it's hard to decipher how to construct the trust chain +2. The chain of trust is not a traditional certificate chain but rather a sequence of signed messages + +## Recommendation and justification + +Proceed with Option B. This is the most readily available and flexible option. diff --git a/docs/source/design/sgx-infrastructure/decisions/enclave-language.md b/docs/source/design/sgx-infrastructure/decisions/enclave-language.md new file mode 100644 index 0000000000..2226116d65 --- /dev/null +++ b/docs/source/design/sgx-infrastructure/decisions/enclave-language.md @@ -0,0 +1,59 @@ +![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png) + +-------------------------------------------- +Design Decision: Enclave language of choice +============================================ + +## Background / Context + +In the long run we would like to use the JVM for all enclave code. This is so that later on we can solve the problem of +side channel attacks on the bytecode level (e.g. oblivious RAM) rather than putting this burden on enclave functionality +implementors. + +As we plan to use a JVM in the long run anyway and we already have an embedded Avian implementation I think the best +course of action is to immediately use this together with the full JDK. To keep the native layer as minimal as possible +we should forward enclave calls with little to no marshalling to the embedded JVM. All subsequent sanity checks, +including ones currently handled by the edger8r generated code should be done inside the JVM. Accessing native enclave +functionality (including OCALLs and reading memory from untrusted heap) should be through a centrally defined JNI +interface. This way when we switch from Avian we have a very clear interface to code against both from the hosted code's +side and from the ECALL/OCALL side. + +The question remains what the thin native layer should be written in. Currently we use C++, but various alternatives +popped up, most notably Rust. + +## Options Analysis + +### A. C++ + +#### Advantages + +1. The Intel SDK is written in C++ +2. [Reproducible binaries](https://wiki.debian.org/ReproducibleBuilds) +3. The native parts of Avian, HotSpot and SubstrateVM are written in C/C++ + +#### Disadvantages + +1. Unsafe memory accesses (unless strict adherence to modern C++) +2. Quirky build +3. Larger attack surface + +### B. Rust + +#### Advantages + +1. ​Safe memory accesses +2. Easier to read/write code, easier to audit + +#### Disadvantages + +1. ​Does not produce reproducible binaries currently (but it's [planned](https://github.com/rust-lang/rust/issues/34902)) +2. ​We would mostly be using it for unsafe things (raw pointers, calling C++ code) + +## Recommendation and justification + +Proceed with Option A (C++) and keep the native layer as small as possible. Rust currently doesn't produce reproducible +binary code, and we need the native layer mostly to handle raw pointers and call Intel SDK functions anyway, so we +wouldn't really leverage Rust's safe memory features. + +Having said that, once Rust implements reproducible builds we may switch to it, in this case the thinness of the native +layer will be of big benefit. diff --git a/docs/source/design/sgx-infrastructure/decisions/kv-store.md b/docs/source/design/sgx-infrastructure/decisions/kv-store.md new file mode 100644 index 0000000000..b74c3342d4 --- /dev/null +++ b/docs/source/design/sgx-infrastructure/decisions/kv-store.md @@ -0,0 +1,58 @@ +![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png) + +-------------------------------------------- +Design Decision: Key-value store implementation +============================================ + +This is a simple choice of technology. + +## Options Analysis + +### A. ZooKeeper + +#### Advantages + +1. Tried and tested +2. HA team already uses ZooKeeper + +#### Disadvantages + +1. Clunky API +2. No HTTP API +3. Handrolled protocol + +### B. etcd + +#### Advantages + +1. Very simple API, UNIX philosophy +2. gRPC +3. Tried and tested +4. MVCC +5. Kubernetes uses it in the background already +6. "Successor" of ZooKeeper +7. Cross-platform, OSX and Windows support +8. Resiliency, supports backups for disaster recovery + +#### Disadvantages + +1. HA team uses ZooKeeper + +### C. Consul + +#### Advantages + +1. End to end discovery including UIs + +#### Disadvantages + +1. Not very well spread +2. Need to store other metadata as well +3. HA team uses ZooKeeper + +## Recommendation and justification + +Proceed with Option B (etcd). It's practically a successor of ZooKeeper, the interface is quite simple, it focuses on +primitives (CAS, leases, watches etc) and is tried and tested by many heavily used applications, most notably +Kubernetes. In fact we have the option to use etcd indirectly by writing Kubernetes extensions, this would have the +advantage of getting readily available CLI and UI tools to manage an enclave cluster. diff --git a/docs/source/design/sgx-infrastructure/decisions/roadmap.md b/docs/source/design/sgx-infrastructure/decisions/roadmap.md new file mode 100644 index 0000000000..c3c4fced17 --- /dev/null +++ b/docs/source/design/sgx-infrastructure/decisions/roadmap.md @@ -0,0 +1,81 @@ +![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png) + +-------------------------------------------- +Design Decision: Strategic SGX roadmap +============================================ + +## Background / Context + +The statefulness of the enclave affects the complexity of both the infrastructure and attestation greatly. +The infrastructure needs to take care of tracking enclave state for request routing, and we need extra care if we want +to make sure that old keys cannot be used to reveal sealed secrets. + +As the first step the easiest thing to do would be to provide an infrastructure for hosting *stateless* enclaves that +are only concerned with enclave to non-enclave attestation. This provides a framework to do provable computations, +without the headache of handling sealed state and the various implied upgrade paths. + +In the first phase we want to facilitate the ease of rolling out full enclave images (JAR linked into the image) +regardless of what the enclaves are doing internally. The contract of an enclave is the host-enclave API (attestation +protocol) and the exposure of the static set of channels the enclave supports. Furthermore the infrastructure will allow +deployment in a cloud environment and trivial scalability of enclaves through starting them on-demand. + +The first phase will allow for a "fixed stateless provable computations as a service" product, e.g. provable builds or +RNG. + +The question remains on how we should proceed afterwards. In terms of infrastructure we have a choice of implementing +sealed state or focusing on dynamic loading of bytecode. We also have the option to delay this decision until the end of +the first phase. + +## Options Analysis + +### A. Implement sealed state + +Implementing sealed state involves solving the routing problem, for this we can use the concept of active channel sets. +Furthermore we need to solve various additional security issues around guarding sealed secret provisioning, most notably +expiration checks. This would involve implementing a future-proof calendar time oracle, which may turn out to be +impossible, or not quite good enough. We may decide that we cannot actually provide strong privacy guarantees and need +to enforce epochs as mentioned [here](../details/time.md). + +#### Advantages + +1. We would solve long term secret persistence early, allowing for a longer timeframe for testing upgrades and + reprovisioning before we integrate Corda +2. Allows "fixed stateful provable computations as a service" product, e.g. HA encryption + +#### Disadvantages + +1. There are some unsolved issues (Calendar time, sealing epochs) +2. It would delay non-stateful Corda integration + +### B. Implement dynamic code loading + +Implementing dynamic loading involves sandboxing of the bytecode, providing bytecode verification and perhaps +storage/caching of JARs (although it may be better to develop a more generic caching layer and use channels themselves +to do the upload). Doing bytecode verification is quite involved as Avian does not support verification, so this +would mean switching to a different JVM. This JVM would either be HotSpot or SubstrateVM, we are doing some preliminary +exploratory work to assess their feasibility. If we choose this path it opens up the first true integration point with +Corda by enabling semi-validating notaries - these are non-validating notaries that check an SGX signature over the +transaction. It would also enable an entirely separate generic product for verifiable pure computation. + +#### Advantages + +1. Early adoption of Graal if we choose to go with it (the alternative is HotSpot) +2. ​Allows first integration with Corda (semi-validating notaries) +3. Allows "generic stateless provable computation as a service" product, i.e. anything expressible as a JAR +4. Holding off on sealed state + +#### Disadvantages + +1. Too early ​Graal integration may result in maintenance headache later + +## Recommendation and justification + +Proceed with Option B, dynamic code loading. It would make us very early adopters of Graal (with the implied ups and +downs), and most importantly kickstart collaboration between R3 and Oracle. We would also move away from Avian which we +wanted to do anyway. It would also give us more time to think about the issues around sealed state, do exploratory work +on potential solutions, and there may be further development from Intel's side. Furthermore we need dynamic loading for +any fully fledged Corda integration, so we should finish this ASAP. + +## Appendix: Proposed roadmap breakdown + +![Dynamic code loading first](roadmap.png) \ No newline at end of file diff --git a/docs/source/design/sgx-infrastructure/decisions/roadmap.png b/docs/source/design/sgx-infrastructure/decisions/roadmap.png new file mode 100644 index 0000000000..038f3430e6 Binary files /dev/null and b/docs/source/design/sgx-infrastructure/decisions/roadmap.png differ diff --git a/docs/source/design/sgx-infrastructure/design.md b/docs/source/design/sgx-infrastructure/design.md new file mode 100644 index 0000000000..2bcdce7b7a --- /dev/null +++ b/docs/source/design/sgx-infrastructure/design.md @@ -0,0 +1,78 @@ +# SGX Infrastructure design + +.. important:: This design document describes a feature of Corda Enterprise. + +This document is intended as a design description of the infrastructure around the hosting of SGX enclaves, interaction +with enclaves and storage of encrypted data. It assumes basic knowledge of SGX concepts, and some knowledge of +Kubernetes for parts specific to that. + +## High level description + +The main idea behind the infrastructure is to provide a highly available cluster of enclave services (hosts) which can +serve enclaves on demand. It provides an interface for enclave business logic that's agnostic with regards to the +infrastructure, similar to [serverless architectures](details/serverless.md). The enclaves will use an opaque reference +to other enclaves or services in the form of [enclave channels](details/channels.md). Channels hides attestation details +and provide a loose coupling between enclave/non-enclave functionality and specific enclave images/services implementing +it. This loose coupling allows easier upgrade of enclaves, relaxed trust (whitelisting), dynamic deployment, and +horizontal scaling as we can spin up enclaves dynamically on demand when a channel is requested. + +## Infrastructure components + +Here are the major components of the infrastructure. Note that this doesn't include business logic specific +infrastructure pieces (like ORAM blob storage for Corda privacy model integration). + +* [**Distributed key-value store**](details/kv-store.md): + Responsible for maintaining metadata about enclaves, hosts, sealed secrets and CPU locality. + +* [**Discovery service**](details/discovery.md) + Responsible for resolving an enclave channel to a specific enclave image and a host that can serve it using the + metadata in the key-value store. + +* [**Enclave host**](details/host.md): + This is a service capable of serving enclaves and driving the underlying traffic. Third party components like Intel's + SGX driver and aesmd also belong here. + +* [**Enclave storage**](details/enclave-storage.md): + Responsible for serving enclave images to hosts. This is a simple static content server. + +* [**IAS proxy**](details/ias-proxy.md): + This is an unfortunate necessity because of Intel's requirement to do mutual TLS with their services. + +## Infrastructure interactions + +* **Enclave deployment**: + This includes uploading of the enclave image/container to enclave storage and adding of the enclave metadata to the + key-value store. + +* **Enclave usage**: + This includes using the discovery service to find a specific enclave image and a host to serve it, then connecting to + the host, authenticating(attestation) and proceeding with the needed functionality. + +* **Ops**: + This includes management of the cluster (Kubernetes/Kubespray) and management of the metadata relating to discovery to + control enclave deployment (e.g. canary, incremental, rollback). + +## Decisions to be made + +* [**Strategic roadmap**](decisions/roadmap.md) +* [**CPU certification method**](decisions/certification.md) +* [**Enclave language of choice**](decisions/enclave-language.md) +* [**Key-value store**](decisions/kv-store.md) + +## Further details + +* [**Attestation**](details/attestation.md) +* [**Calendar time for data at rest**](details/time.md) +* [**Enclave deployment**](details/enclave-deployment.md) + +## Example deployment + +This is an example of how two Corda parties may use the above infrastructure. In this example R3 is hosting the IAS +proxy and the enclave image store and the parties host the rest of the infrastructure, aside from Intel components. + +Note that this is flexible, the parties may decide to host their own proxies (as long as they whitelist their keys) or +the enclave image store (although R3 will need to have a repository of the signed enclaves somewhere). +We may also decide to go the other way and have R3 host the enclave hosts and the discovery service, shared between +parties (if e.g. they don't have access to/want to maintain SGX capable boxes). + +![Example SGX deployment](Example%20SGX%20deployment.png) \ No newline at end of file diff --git a/docs/source/design/sgx-infrastructure/details/attestation.md b/docs/source/design/sgx-infrastructure/details/attestation.md new file mode 100644 index 0000000000..8148e8c63b --- /dev/null +++ b/docs/source/design/sgx-infrastructure/details/attestation.md @@ -0,0 +1,92 @@ +### Terminology recap + +**measurement**: The hash of an enclave image, uniquely pinning the code and related configuration +**report**: A datastructure produced by an enclave including the measurement and other non-static properties of the + running enclave instance (like the security version number of the hardware) +**quote**: A signed report of an enclave produced by Intel's quoting enclave. + +# Attestation + +The goal of attestation is to authenticate enclaves. We are concerned with two variants of this, enclave to non-enclave +attestation and enclave to enclave attestation. + +In order to authenticate an enclave we need to establish a chain of trust rooted in an Intel signature certifying that a +report is coming from an enclave running on genuine Intel hardware. + +Intel's recommended attestation protocol is split into two phases. + +1. Provisioning +The first phase's goal is to establish an Attestation Key(AK) aka EPID key, unique to the SGX installation. +The establishment of this key uses an underdocumented protocol similar to the attestation protocol: + - Intel provides a Provisioning Certification Enclave(PCE). This enclave has special privileges in that it can derive a + key in a deterministic fashion based on the *provisioning* fuse values. Intel stores these values in their databases + and can do the same derivation to later check a signature from PCE. + - Intel provides a separate enclave called the Provisioning Enclave(PvE), also privileged, which interfaces with PCE + (using local attestation) to certify the PvE's report and talks with a special Intel endpoint to join an EPID group + anonymously. During the join Intel verifies the PCE's signature. Once the join happened the PvE creates a related + private key(the AK) that cannot be linked by Intel to a specific CPU. The PvE seals this key (also sometimes referred + to as the "EPID blob") to MRSIGNER, which means it can only be unsealed by Intel enclaves. + +2. Attestation + - When a user wants to do attestation of their own enclave they need to do so through the Quoting Enclave(QE), also + signed by Intel. This enclave can unseal the EPID blob and use the key to sign over user provided reports + - The signed quote in turn is sent to the Intel Attestation Service, which can check whether the quote was signed by a + key in the EPID group. Intel also checks whether the QE was provided with an up-to-date revocation list. + +The end result is a signature of Intel over a signature of the AK over the user enclave quote. Challengers can then +simply check this chain to make sure that the user provided data in the quote (probably another key) comes from a +genuine enclave. + +All enclaves involved (PCE, PvE, QE) are owned by Intel, so this setup basically forces us to use Intel's infrastructure +during attestation (which in turn forces us to do e.g. MutualTLS, maintain our own proxies etc). There are two ways we +can get around this. + +1. Hook the provisioning phase. During the last step of provisioning the PvE constructs a chain of trust rooted in + Intel. If we can extract some provable chain that allows proving of membership based on an EPID signature then we can + essentially replicate what IAS does. +2. Bootstrap our own certification. This would involve deriving another certification key based on sealing fuse values + and getting an Intel signature over it using the original IAS protocol. This signature would then serve the same + purpose as the certificate in 1. + +## Non-enclave to enclave channels + +When a non-enclave connects to a "leaf" enclave the goal is to establish a secure channel between the non-enclave and +the enclave by authenticating the enclave and possibly authenticating the non-enclave. In addition we want to provide +secrecy of the non-enclave. To this end we can use SIGMA-I to do a Diffie-Hellman key exchange between the non-enclave +identity and the enclave identity. + +The enclave proves the authenticity of its identity by providing a certificate chain rooted in Intel. If we do our own +enclave certification then the chain goes like this: + +* Intel signs quote of certifying enclave containing the certifying key pair's public part. +* Certifying key signs report of leaf enclave containing the enclave's temporary identity. +* Enclave identity signs the relevant bits in the SIGMA protocol. + +Intel's signature may be cached on disk, and the certifying enclave signature over the temporary identity may be cached +in enclave memory. + +We can provide various invalidations, e.g. non-enclave won't accept signature if X time has passed since Intel's +signature, or R3's whitelisting cert expired etc. + +If the enclave needs to authorise the non-enclave the situation is a bit more complicated. Let's say the enclave holds +some secret that it should only reveal to authorised non-enclaves. Authorisation is expressed as a whitelisting +signature over the non-enclave identity. How do we check the expiration of the whitelisting key's certificate? + +Calendar time inside enclaves deserves its own [document](time.md), the gist is that we simply don't have access to time +unless we trust a calendar time oracle. + +Note however that we probably won't need in-enclave authorisation for *stateless* enclaves, as these have no secrets to +reveal at all. Authorisation would simply serve as access control, and we can solve access control in the hosting +infrastructure instead. + +## Enclave to enclave channels + +Doing remote attestation between enclaves is similar to enclave to non-enclave, only this time authentication involves +verifying the chain of trust on both sides. However note that this is also predicated on having access to a calendar +time oracle, as this time expiration checks of the chain must be done in enclaves. So in a sense both enclave to enclave +and stateful enclave to non-enclave attestation forces us to trust a calendar time oracle. + +But note that remote enclave to enclave attestation is mostly required when there *is* sealed state (secrets to share +with the other enclave). One other use case is the reduction of audit surface, once it comes to that. We may be able to +split stateless enclaves into components that have different upgrade lifecycles. By doing so we ease the auditors' job +by reducing the enclaves' contracts and code size. diff --git a/docs/source/design/sgx-infrastructure/details/channels.md b/docs/source/design/sgx-infrastructure/details/channels.md new file mode 100644 index 0000000000..367ce6de3e --- /dev/null +++ b/docs/source/design/sgx-infrastructure/details/channels.md @@ -0,0 +1,75 @@ +# Enclave channels + +AWS Lambdas may be invoked by name, and are simple request-response type RPCs. The lambda's name abstracts the +specific JAR or code image that implements the functionality, which allows upgrading of a lambda without disrupting +the rest of the lambdas. + +Any authentication required for the invocation is done by a different AWS service (IAM), and is assumed to be taken +care of by the time the lambda code is called. + +Serverless enclaves also require ways to be addressed, let's call these "enclave channels". Each such channel may be +identified with a string similar to Lambdas, however unlike lambdas we need to incorporate authentication into the +concept of a channel in the form of attestation. + +Furthermore unlike Lambdas we can implement a generic two-way communication channel. This reintroduces state into the +enclave logic. However note that this state is in-memory only, and because of the transient nature of enclaves (they +may be "lost" at any point) enclave authors are in general incentivised to either keep in-memory state minimal (by +sealing state) or make their functionality idempotent (allowing retries). + +We should be able to determine an enclave's supported channels statically. Enclaves may store this data for example in a +specific ELF section or a separate file. The latter may be preferable as it may be hard to have a central definition of +channels in an ELF section if we use JVM bytecode. Instead we could have a specific static JVM datastructure that can be +extracted from the enclave statically during the build. + +## Sealed state + +Sealing keys tied to specific CPUs seem to throw a wrench in the requirement of statelessness. Routing a request to an +enclave that has associated sealed state cannot be the same as routing to one which doesn't. How can we transparently +scale enclaves like Lambdas if fresh enclaves by definition don't have associated sealed state? + +Take key provisioning as an example: we want some key to be accessible by a number of enclaves, how do we +differentiate between enclaves that have the key provisioned versus ones that don't? We need to somehow expose an +opaque version of the enclave's sealed state to the hosting infrastructure for this. + +The way we could do this is by expressing this state in terms of a changing set of "active" enclave channels. The +enclave can statically declare the channels it potentially supports, and start with some initial subset of them as +active. As the enclave's lifecycle (sealed state) evolves it may change this active set to something different, +thereby informing the hosting infrastructure that it shouldn't route certain requests there, or that it can route some +other ones. + +Take the above key provisioning example. An enclave can be in two states, unprovisioned or provisioned. When it's +unprovisioned its set of active channels will be related to provisioning (for example, request to bootstrap key or +request from sibling enclave), when it's provisioned its active set will be related to the usage of the key and +provisioning of the key itself to unprovisioned enclaves. + +The enclave's initial set of active channels defines how enclaves may be scaled horizontally, as these are the +channels that will be active for the freshly started enclaves without sealed state. + +"Hold on" you might say, "this means we didn't solve the scalability of stateful enclaves!". + +This is partly true. However in the above case we can force certain channels to be part of the initial active set! In +particular the channels that actually use the key (e.g. for signing) may be made "stateless" by lazily requesting +provisioning of the key from sibling enclaves. Enclaves may be spun up on demand, and as long as there is at least one +sibling enclave holding the key it will be provisioned as needed. This hints at a general pattern of hiding stateful +functionality behind stateless channels, if we want them to scale automatically. + +Note that this doesn't mean we can't have external control over the provisioning of the key. For example we probably +want to enforce redundancy across N CPUs. This requires the looping in of the hosting infrastructure, we cannot +enforce this invariant purely in enclave code. + +As we can see the set of active enclave channels are inherently tied to the sealed state of the enclave, therefore we +should make the updating both of them an atomic operation. + +### Side note + +Another way to think about enclaves using sealed state is like an actor model. The sealed state is the actor's state, +and state transitions may be executed by any enclave instance running on the same CPU. By transitioning the actor state +one can also transition the type of messages the actor can receive atomically (= active channel set). + +## Potential gRPC integration + +It may be desirable to expose a built-in serialisation and network protocol. This would tie us to a specific protocol, +but in turn it would ease development. + +An obvious candidate for this is gRPC as it supports streaming and a specific serialization protocol. We need to +investigate how we can integrate it so that channels are basically responsible for tunneling gRPC packets. diff --git a/docs/source/design/sgx-infrastructure/details/discovery.md b/docs/source/design/sgx-infrastructure/details/discovery.md new file mode 100644 index 0000000000..3ed2a18192 --- /dev/null +++ b/docs/source/design/sgx-infrastructure/details/discovery.md @@ -0,0 +1,88 @@ +# Discovery + +In order to understand enclave discovery and routing we first need to understand the mappings between CPUs, VMs and +enclave hosts. + +The cloud provider manages a number of physical machines (CPUs), each of those machines hosts a hypervisor which in +turn hosts a number of guest VMs. Each VM in turn may host a number of enclave host containers (together with required +supporting software like aesmd) and the sgx device driver. Each enclave host in turn may host several enclave instances. +For the sake of simplicity let's assume that an enclave host may only host a single enclave instance per measurement. + +We can figure out the identity of the CPU the VM is running on by using a dedicated enclave to derive a unique ID +specific to the CPU. For this we can use EGETKEY with pre-defined inputs to derive a seal key sealed to MRENCLAVE. This +provides a 128bit value reproducible only on the same CPU in this manner. Note that this is completely safe as the +value won't be used for encryption and is specific to the measurement doing this. With this ID we can reason about +physical locality of enclaves without looping in the cloud provider. +Note: we should set OWNEREPOCH to a static value before doing this. + +We don't need an explicit handle on the VM's identity, the mapping from VM to container will be handled by the +orchestration engine (Kubernetes). + +Similarly to VM identity, the specific host container's identity(IP address/DNS A) is also tracked by Kubernetes, +however we do need access to this identity in order to implement discovery. + +When an enclave instance seals a secret that piece of data is tied to the measurement+CPU combo. The secret can only be +revealed to an enclave with the same measurement running on the same CPU. However the management of this secret is +tied to the enclave host container, which we may have several of running on the same CPU, possibly all of them hosting +enclaves with the same measurement. + +To solve this we can introduce a *sealing identity*. This is basically a generated ID/namespace for a collection of +secrets belonging to a specific CPU. It is generated when a fresh enclave host starts up and subsequently the host will +store sealed secrets under this ID. These secrets should survive host death, so they will be persisted in etcd (together +with the associated active channel sets). Every host owns a single sealing identity, but not every sealing identity may +have an associated host (e.g. in case the host died). + +## Mapping to Kubernetes + +The following mapping of the above concepts to Kubernetes concepts is not yet fleshed out and requires further +investigation into Kubernetes capabilities. + +VMs correspond to Nodes, and enclave hosts correspond to Pods. The host's identity is the same as the Pod's, which is +the Pod's IP address/DNS A record. From Kubernetes's point of view enclave hosts provide a uniform stateless Headless +Service. This means we can use their scaling/autoscaling features to provide redundancy across hosts (to balance load). + +However we'll probably need to tweak their (federated?) ReplicaSet concept in order to provide redundancy across CPUs +(to be tolerant of CPU failures), or perhaps use their anti-affinity feature somehow, to be explored. + +The concept of a sealing identity is very close to the stable identity of Pods in Kubernetes StatefulSets. However I +couldn't find a way to use this directly as we need to tie the sealing identity to the CPU identity, which in Kubernetes +would translate to a requirement to pin stateful Pods to Nodes based on a dynamically determined identity. We could +however write an extension to handle this metadata. + +## Registration + +When an enclave host is started it first needs to establish its sealing identity. To this end first it needs to check +whether there are any sealing identities available for the CPU it's running on. If not it can generate a fresh one and +lease it for a period of time (and update the lease periodically) and atomically register its IP address in the process. +If an existing identity is available the host can take over it by leasing it. There may be existing Kubernetes +functionality to handle some of this. + +Non-enclave services (like blob storage) could register similarly, but in this case we can take advantage of Kubernetes' +existing discovery infrastructure to abstract a service behind a Service cluster IP. We do need to provide the metadata +about supported channels though. + +## Resolution + +The enclave/service discovery problem boils down to: +"Given a channel, my trust model and my identity, give me an enclave/service that serves this channel, trusts me, and I +trust them". + +This may be done in the following steps: + +1. Resolve the channel to a set of measurements supporting it +2. Filter the measurements to trusted ones and ones that trust us +3. Pick one of the measurements randomly +4. Find an alive host that has the channel in its active set for the measurement + +1 may be done by maintaining a channel -> measurements map in etcd. This mapping would effectively define the enclave +deployment and would be the central place to control incremental rollout or rollbacks. + +2 requires storing of additional metadata per advertised channel, namely a datastructure describing the enclave's trust +predicate. A similar datastructure is provided by the discovering entity - these two predicates can then be used to +filter measurements based on trust. + +3 is where we may want to introduce more control if we want to support incremental rollout/canary deployments. + +4 is where various (non-MVP) optimisation considerations come to mind. We could add a loadbalancer, do autoscaling based +on load (although Kubernetes already provides support for this), could have a preference for looping back to the same +host to allow local attestation, or ones that have the enclave image cached locally or warmed up. diff --git a/docs/source/design/sgx-infrastructure/details/enclave-deployment.md b/docs/source/design/sgx-infrastructure/details/enclave-deployment.md new file mode 100644 index 0000000000..905bab9930 --- /dev/null +++ b/docs/source/design/sgx-infrastructure/details/enclave-deployment.md @@ -0,0 +1,16 @@ +# Enclave deployment + +What happens if we roll out a new enclave image? + +In production we need to sign the image directly with the R3 key as MRSIGNER (process to be designed), as well as create +any whitelisting signatures needed (e.g. from auditors) in order to allow existing enclaves to trust the new one. + +We need to make the enclave build sources available to users - we can package this up as a single container pinning all +build dependencies and source code. Docker style image layering/caching will come in handy here. + +Once the image, build containers and related signatures are created we need to push this to the main R3 enclave storage. + +Enclave infrastructure owners (e.g. Corda nodes) may then start using the images depending on their upgrade policy. This +involves updating their key value store so that new channel discovery requests resolve to the new measurement, which in +turn will trigger the image download on demand on enclave hosts. We can potentially add pre-caching here to reduce +latency for first-time enclave users. diff --git a/docs/source/design/sgx-infrastructure/details/enclave-storage.md b/docs/source/design/sgx-infrastructure/details/enclave-storage.md new file mode 100644 index 0000000000..85db88363b --- /dev/null +++ b/docs/source/design/sgx-infrastructure/details/enclave-storage.md @@ -0,0 +1,7 @@ +# Enclave storage + +The enclave storage is a simple static content server. It should allow uploading of and serving of enclave images based +on their measurement. We may also want to store metadata about the enclave build itself (e.g. github link/commit hash). + +We may need to extend its responsibilities to serve other SGX related static content such as whitelisting signatures +over measurements. diff --git a/docs/source/design/sgx-infrastructure/details/host.md b/docs/source/design/sgx-infrastructure/details/host.md new file mode 100644 index 0000000000..4c8475673e --- /dev/null +++ b/docs/source/design/sgx-infrastructure/details/host.md @@ -0,0 +1,11 @@ +# Enclave host + +An enclave host's responsibility is the orchestration of the communication with hosted enclaves. + +It is responsible for: +* Leasing a sealing identity +* Getting a CPU certificate in the form of an Intel-signed quote +* Downloading and starting of requested enclaves +* Driving attestation and subsequent encrypted traffic +* Using discovery to connect to other enclaves/services +* Various caching layers (and invalidation of) for the CPU certificate, hosted enclave quotes and enclave images diff --git a/docs/source/design/sgx-infrastructure/details/ias-proxy.md b/docs/source/design/sgx-infrastructure/details/ias-proxy.md new file mode 100644 index 0000000000..1d33954079 --- /dev/null +++ b/docs/source/design/sgx-infrastructure/details/ias-proxy.md @@ -0,0 +1,10 @@ +# IAS proxy + +The Intel Attestation Service proxy's responsibility is simply to forward requests to and from the IAS. + +The reason we need this proxy is because Intel requires us to do Mutual TLS with them for each attestation roundtrip. +For this we need an R3 maintained private key, and as we want third parties to be able to do attestation we need to +store this private key in these proxies. + +Alternatively we may decide to circumvent this mutual TLS requirement completely by distributing the private key with +the host containers. \ No newline at end of file diff --git a/docs/source/design/sgx-infrastructure/details/kv-store.md b/docs/source/design/sgx-infrastructure/details/kv-store.md new file mode 100644 index 0000000000..fef06d7e03 --- /dev/null +++ b/docs/source/design/sgx-infrastructure/details/kv-store.md @@ -0,0 +1,13 @@ +# Key-value store + +To solve enclave to enclave and enclave to non-enclave communication we need a way to route requests correctly. There +are readily available discovery solutions out there, however we have some special requirements because of the inherent +statefulness of enclaves (route to enclave with correct state) and the dynamic nature of trust between them (route to +enclave I can trust and that trusts me). To store metadata about discovery we can need some kind of distributed +key-value store. + +The key-value store needs to store information about the following entities: +* Enclave image: measurement and supported channels +* Sealing identity: the sealing ID, the corresponding CPU ID and the host leasing it (if any) +* Sealed secret: the sealing ID, the sealing measurement, the sealed secret and corresponding active channel set +* Enclave deployment: mapping from channel to set of measurements diff --git a/docs/source/design/sgx-infrastructure/details/serverless.md b/docs/source/design/sgx-infrastructure/details/serverless.md new file mode 100644 index 0000000000..7ef031d3e7 --- /dev/null +++ b/docs/source/design/sgx-infrastructure/details/serverless.md @@ -0,0 +1,33 @@ +# Serverless architectures + +In 2014 Amazon launched AWS Lambda, which they coined a "serverless architecture". It essentially creates an abstraction +layer which hides the infrastructure details. Users provide "lambdas", which are stateless functions that may invoke +other lambdas, access other AWS services etc. Because Lambdas are inherently stateless (any state they need must be +accessed through a service) they may be loaded and executed on demand. This is in contrast with microservices, which +are inherently stateful. Internally AWS caches the lambda images and even caches JIT compiled/warmed up code in order +to reduce latency. Furthermore the lambda invokation interface provides a convenient way to scale these lambdas: as the +functions are statelesss AWS can spin up new VMs to push lambda functions to. The user simply pays for CPU usage, all +the infrastructure pain is hidden by Amazon. + +Google and Microsoft followed suit in a couple of years with Cloud Functions and Azure Functions. + +This way of splitting hosting computation from a hosted restricted computation is not a new idea, examples are web +frameworks (web server vs application), MapReduce (Hadoop vs mappers/reducers), or even the cloud (hypervisors vs vms) +and the operating system (kernel vs userspace). The common pattern is: the hosting layer hides some kind of complexity, +imposes some restriction on the guest layer (and provides a simpler interface in turn), and transparently multiplexes +a number of resources for them. + +The relevant key features of serverless architectures are 1. on-demand scaling and 2. business logic independent of +hosting logic. + +# Serverless SGX? + +How are Amazon Lambdas relevant to SGX? Enclaves exhibit very similar features to Lambdas: they are pieces of business +logic completely independent of the hosting functionality. Not only that, enclaves treat hosts as adversaries! This +provides a very clean separation of concerns which we can exploit. + +If we could provide a similar infrastructure for enclaves as Amazon provides for Lambdas it would not only allow easy +HA and scaling, it would also decouple the burden of maintaining the infrastructure from the enclave business logic. +Furthermore our plan of using the JVM within enclaves also aligns with the optimizations Amazon implemented (e.g. +keeping warmed up enclaves around). Optimizations like upgrading to local attestation also become orthogonal to +enclave business logic. Enclave code can focus on the specific functionality at hand, everything else is taken care of. diff --git a/docs/source/design/sgx-infrastructure/details/time.md b/docs/source/design/sgx-infrastructure/details/time.md new file mode 100644 index 0000000000..064e59562c --- /dev/null +++ b/docs/source/design/sgx-infrastructure/details/time.md @@ -0,0 +1,69 @@ +# Time in enclaves + +In general we know that any one crypto algorithm will be broken in X years time. The usual way to mitigate this is by +using certificate expiration. If a peer with an expired certificate tries to connect we reject it in order to enforce +freshness of their key. + +In order to check certificate expiration we need some notion of calendar time. However in SGX's threat model the host +of the enclave is considered malicious, so we cannot rely on their notion of time. Intel provides trusted time through +their PSW, however this uses the Management Engine which is known to be a proprietary vulnerable piece of architecture. + +Therefore in order to check calendar time in general we need some kind of time oracle. We can burn in the oracle's +identity to the enclave and request timestamped signatures from it. This already raises questions with regards to the +oracle's identity itself, however for the time being let's assume we have something like this in place. + +### Timestamped nonces + +The most straightforward way to implement calendar time checks is to generate a nonce *after* DH exchange, send it to +the oracle and have it sign over it with a timestamp. The nonce is required to avoid replay attacks. A malicious host +may delay the delivery of the signature indefinitely, even until after the certificate expires. However note that the +DH happened before the nonce was generated, which means even if an attacker can crack the expired key they would not be +able to steal the DH session, only try creating new ones, which will fail at the timestamp check. + +This seems to be working, however note that this would impose a full roundtrip to an oracle *per DH exchange*. + +### Timestamp-encrypted channels + +In order to reduce the roundtrips required for timestamp checking we can invert the responsibility of checking of the +timestamp. We can do this by encrypting the channel traffic with an additional key generated by the enclave but that can +only be revealed by the time oracle. The enclave encrypts the encryption key with the oracle's public key so the peer +trying to communicate with the enclave must forward the encrypted key to the oracle. The oracle in turn will check the +timestamp and reveal the contents (perhaps double encrypted with a DH-derived key). The peer can cache the key and later +use the same encryption key with the enclave. It is then the peer's responsibility to get rid of the key after a while. + +Note that this mitigates attacks where the attacker is a third party trying to exploit an expired key, but this method +does *not* mitigate against malicious peers that keep around the encryption key until after expiration(= they "become" +malicious). + +### Oracle key break + +So given an oracle we can secure a channel against expired keys and potentially improve performance by trusting +once-authorized enclave peers to not become malicious. + +However what happens if the oracle key itself is broken? There's a chicken-and-egg problem where we can't check the +expiration of the time oracle's certificate itself! Once the oracle's key is broken an attacker can fake timestamping +replies (or decrypt the timestamp encryption key), which in turn allows it to bypass the expiration check. + +The main issue with this is in relation to sealed secrets, and sealed secret provisioning between enclaves. If an +attacker can fake being e.g. an authorized enclave then it can extract old secrets. We have yet to come up with a +solution to this, and I don't think it's possible. + +Instead, knowing that current crypto algorithms are bound to be broken at *some* point in the future, instead of trying +to make sealing future-proof we can become explicit about the time-boundness of security guarantees. + +### Sealing epochs + +Let's call the time period in which a certain set of algorithms are considered safe a *sealing epoch*. During this +period sealed data at rest is considered to be secure. However once the epoch finishes old sealed data is considered to +be potentially compromised. We can then think of sealed data as an append-only log of secrets with overlapping epoch +intervals where the "breaking" of old epochs is constantly catching up with new ones. + +In order to make sure that this works we need to enforce an invariant where secrets only flow from old epochs to newer +ones, never the other way around. + +This translates to the ledger nicely, data in old epochs are generally not valuable anymore, so it's safe to consider +them compromised. Note however that in the privacy model an epoch transition requires a full re-provisioning of the +ledger to the new set of algorithms/enclaves. + +In any case this is an involved problem, and I think we should defer the fleshing out of it for now as we won't need it +for the first round of stateless enclaves. diff --git a/docs/source/design/sgx-integration/SgxProvisioning.png b/docs/source/design/sgx-integration/SgxProvisioning.png new file mode 100644 index 0000000000..2c52a3f180 Binary files /dev/null and b/docs/source/design/sgx-integration/SgxProvisioning.png differ diff --git a/docs/source/design/sgx-integration/design.md b/docs/source/design/sgx-integration/design.md new file mode 100644 index 0000000000..daee03273d --- /dev/null +++ b/docs/source/design/sgx-integration/design.md @@ -0,0 +1,315 @@ +This document is intended as a design description of how we can go about integrating SGX with Corda. As the +infrastructure design of SGX is quite involved (detailed elsewhere) but otherwise flexible we can discuss the possible +integration points separately, without delving into lower level technical detail. + +For the purposes of this document we can think of SGX as a way to provision secrets to a remote node with the +knowledge that only trusted code(= enclave) will operate on it. Furthermore it provides a way to durably encrypt data +in a scalable way while also ensuring that the encryption key is never leaked (unless the encrypting enclave is +compromised). + +Broadly speaking there are two dimensions to deciding how we can integrate SGX: *what* we store in the ledger and +*where* we store it. + +The first dimension is the what: this relates to what we so far called the "integrity model" vs the "privacy model". + +In the **integrity model** we rely on SGX to ensure the integrity of the ledger. Using this assumption we can cut off +the transaction body and only store an SGX-backed signature over filtered transactions. Namely we would only store +information required for notarisation of the current and subsequent spending transactions. This seems neat on first +sight, however note that if we do this naively then if an attacker can impersonate an enclave they'll gain write +access to the ledger, as the fake enclave can sign transactions as valid without having run verification. + +In the **privacy model** we store the full transaction backchain (encrypted) and we keep provisioning it between nodes +on demand, just like in the current Corda implementation. This means we only rely on SGX for the privacy aspects - if +an enclave is compromised we only lose privacy, the verification cannot be eluded by providing a fake signature. + +The other dimension is the where: currently in non-SGX Corda the full transaction backchain is provisioned between non- +notary nodes, and is also provisioned to notaries in the case they are validating ones. With SGX+BFT notaries we have +the possibility to offload the storage of the encrypted ledger (or encrypted signatures thereof) to notary nodes (or +dedicated oracles) and only store bookkeeping information required for further ledger updates in non-notary nodes. The +storage policy is very important, customers want control over the persistence of even encrypted data, and with the +introduction of recent regulation (GDPR) unrestricted provisioning of sensitive data will be illegal by law, even when +encrypted. + +We'll explore the different combination of choices below. Note that we don't need to marry to any one of them, we may +decide to implement several. + +## Privacy model + non-notary provisioning + +Let's start with the model that's closest to the current Corda implementation as this is an easy segue into the +possibilities with SGX. We also have a simple example and a corresponding neat diagram (thank you Kostas!!) we showed +to a member bank Itau to indicate in a semi-handwavy way what the integration will look like. + +We have a cordapp X used by node A and B. The cordapp contains a flow XFlow and a (deterministic) contract XContract. +The two nodes are negotiating a transaction T2. T2 consumes a state that comes from transaction T1. + +Let's assume that both A and B are happy with T2, except Node A hasn't established the validity of it yet. Our goal is +to prove the validity of T2 to A without revealing the details of T1. + +The following diagram shows an overview of how this can be achieved. Note that the diagram is highly oversimplified +and is meant to communicate the high-level dataflow relevant to Corda. + +![SGX Provisioning](SgxProvisioning.png "SGX Provisioning") + +* In order to validate T2, A asks its enclave whether T2 is valid. +* The enclave sees that T2 depends on T1, so it consults its sealed ledger whether it contains T1. +* If it does then this means T1 has been verified already, so the enclave moves on to the verification of T2. +* If the ledger doesn't contain T1 then the enclave needs to retrieve it from node B. +* In order to do this A's enclave needs to prove to B's enclave that it is indeed a trusted enclave B can provision T1 + to. This proof is what the attestation process provides. +* Attestation is done in the clear: (TODO attestation diagram) + * A's enclave generates a keypair, the public part of which is sent to Node B in a datastructure signed by Intel, + this is called the quote(1). + * Node B's XFlow may do various checks on this datastructure that cannot be performed by B's enclave, for example + checking of the timeliness of Intel's signature(2). + * Node B's XFlow then forwards the quote to B's enclave, which will check Intel's signature and whether it trusts A' + s enclave. For the sake of simplicity we can assume this to be strict check that A is running the exact same + enclave B is. + * At this point B's enclave has established trust in A's enclave, and has the public part of the key generated by A' + s enclave. + * The nodes repeat the above process the other way around so that A's enclave establishes trust in B's and gets hold + of B's public key(3). + * Now they proceed to perform an ephemeral Diffie-Hellman key exchange using the keys in the quotes(4). + * The ephemeral key is then used to encrypt further communication. Beyond this point the nodes' flows (and anything + outside of the enclaves) have no way of seeing what data is being exchanged, all the nodes can do is forward the + encrypted messages. +* Once attestation is done B's enclave provisions T1 to A's enclave using the DH key. If there are further + dependencies those would be provisioned as well. +* A's enclave then proceeds to verify T1 using the embedded deterministic JVM to run XContract. The verified + transaction is then sealed to disk(5). We repeat this for T2. +* If verification or attestation fails at any point the enclave returns to A's XFlow with a failure. Otherwise if all + is good the enclave returns with a success. At this point A's XFlow knows that T2 is valid, but hasn't seen T1 in + the clear. + +(1) This is simplified, the actual protocol is a bit different. Namely the quote is not generated every time A requires provisioning, but is rather generated periodically. + +(2) There is a way to do this check inside the enclave, however it requires switching on of the Intel ME which in general isn't available on machines in the cloud and is known to have vulnerabilities. + +(3) We need symmetric trust even if the secrets seem to only flow from B to A. Node B may try to fake being an enclave to fish for information from A. + +(4) The generated keys in the quotes are used to authenticate the respective parts of the DH key exchange. + +(5) Sealing means encryption of data using a key unique to the enclave and CPU. The data may be subsequently unsealed (decrypted) by the enclave, even if the enclave was restarted. Also note that there is another layer of abstraction needed which we don't detail here, needed for redundancy of the encryption key. + +To summarise, the journey of T1 is: + +1. Initially it's sitting encrypted in B's storage. +2. B's enclave decrypts it using its seal key specific to B's enclave + CPU combination. +3. B's enclave encrypts it using the ephemeral DH key. +4. The encrypted transaction is sent to A. The safety of this (namely that A's enclave doesn't reveal the transaction to node A) hinges on B's enclave's trust in A's enclave, which is expressed as a check of A's enclave measurement during attestation, which in turn requires auditing of A's enclave code and reproducing of the measurement. +5. A's enclave decrypts the transaction using the DH key. +6. A's enclave verifies the transaction using a deterministic JVM. +7. A's enclave encrypts the transaction using A's seal key specific to A's enclave + CPU combination. +8. The encrypted transaction is stored in A's storage. + +As we can see in this model each non-notary node runs their own SGX enclave and related storage. Validation of the +backchain happens by secure provisioning of it between enclaves, plus subsequent verification and storage. However +there is one important thing missing from the example (actually it has several, but those are mostly technical detail): +the notary! + +In reality we cannot establish the full validity of T2 at this point of negotiation, we need to first notarise it. +This model gives us some flexibility in this regard: we can use a validating notary (also running SGX) or a +non-validating one. This indicates that the enclave API should be split in two, mirroring the signature check choice +in SignedTransaction.verify. Only when the transaction is fully signed and notarised should it be persisted (sealed). + +This model has both advantages and disadvantages. On one hand it is the closest to what we have now - we (and users) +are familiar with this model, we can fairly easily nest it into the existing codebase and it gives us flexibility with +regards to notary modes. On the other hand it is a compromising answer to the regulatory problem. If we use non- +validating notaries then the backchain storage is restricted to participants, however consider the following example: +if we have a transaction X that parties A and B can process legally, but a later transaction Y that has X in its +backchain is sent for verification to party C, then C will process and store X as well, which may be illegal. + +## Privacy model + notary provisioning + +This model would work similarly to the previous one, except non-notary nodes wouldn't need to run SGX or care about +storage of the encrypted ledger, it would all be done in notary nodes. Nodes would connect to SGX capable notary nodes, +and after attestation the nodes can be sure that the notary has run verification before signing. + +This fixes the choice of using validating notaries, as notaries would be the only entities capable of verification: +only they have access to the full backchain inside enclaves. + +Note that because we still provision the full backchain between notary members for verification, we don't necessarily +need a BFT consensus on validity - if an enclave is compromised an invalid transaction will be detected at the next +backchain provisioning. + +This model reduces the number of responsibilities of a non-notary node, in particular it wouldn't need to provide +storage for the backchain or verification, but could simply trust notary signatures. Also it wouldn't need to host SGX +enclaves, only partake in the DH exchange with notary enclaves. The node's responsibilities would be reduced to the +orchestration of ledger updates (flows) and related bookkeeping (vault, network map). This split would also enable us +to be flexible with regards to the update orchestration: trust in the validity of the ledger would cease to depend on +the transaction resolution currently embedded into flows - we could provide a from-scratch light-weight implementation +of a "node" (say a mobile app) that doesn't use flows and related code at all, it just needs to be able to connect to +notary enclaves to notarise, validity is taken care of by notaries. + +Note that although we wouldn't require validation checks from non-notary nodes, in theory it would be safe to allow +them to do so (if they want a stronger-than-BFT guarantee). + +Of course this model has disadvantages too. From the regulatory point of view it is a strictly worse solution than the +non-notary provisioning model: the backchain would be provisioned between notary nodes not owned by actual +participants in the backchain. It also disables us from using non-validating notaries. + +## Integrity model + non-notary provisioning + +In this model we would trust SGX-backed signatures and related attestation datastructures (quote over signature key +signed by Intel) as proof of validity. When node A and B are negotiating a transaction it's enough to provision SGX +signatures over the dependency hashes to one another, there's no need to provision the full backchain. + +This sounds very simple and efficient, and it's even more private than the privacy model as we're only passing +signatures around, not transactions. However there are a couple of issues that need addressing: If an SGX enclave is +compromised a malicious node can provide a signature over an invalid transaction that checks out, and nobody will ever +know about it, because the original transaction will never be verified. One way we can mitigate this is by requiring a +BFT consensus signature, or perhaps a threshold signature is enough. We could decouple verification into "verifying +oracles" which verify in SGX and return signatures over transaction hashes, and require a certain number of them to +convince the notary to notarise and subsequent nodes to trust validity. Another issue is enclave updates. If we find a +vulnerability in an enclave and update it, what happens to the already signed backchain? Historical transactions have +signatures that are rooted in SGX quotes belonging to old untrusted enclave code. One option is to simply have a +cutoff date before which we accept old signatures. This requires a consensus-backed timestamp on the notary signature. +Another option would be to keep the old ledger around and re-verify it with the new enclaves. However if we do this we +lose the benefits of the integrity model - we get back the regulatory issue, and we don't gain the performance benefits. + +## Integrity model + notary provisioning + +This is similar to the previous model, only once again non-notary nodes wouldn't need to care about verifying or +collecting proofs of validity before sending the transaction off for notarisation. All of the complexity would be +hidden by notary nodes, which may use validating oracles or perhaps combine consensus over validity with consensus +over spending. This model would be a very clean separation of concerns which solves the regulatory problem (almost) +and is quite efficient as we don't need to keep provisioning the chain. One potential issue with regards to regulation +is the tip of the ledger (the transaction being notarised) - this is sent to notaries and although it is not stored it +may still be against the law to receive it and hold it in volatile memory, even inside an enclave. I'm unfamiliar with +the legal details of whether this is good enough. If this is an issue, one way we could address this would be to scope +the validity checks required for notarisation within legal boundaries and only require "full" consensus on the +spentness check. Of course this has the downside that ledger participants outside of the regulatory boundary need to +trust the BFT-SGX of the scope. I'm not sure whether it's possible to do any better, after all we can't send the +transaction body outside the scope in any shape or form. + +## Threat model + +In all models we have the following actors, which may or may not overlap depending on the model: + +* Notary quorum members +* Non-notary nodes/entities interacting with the ledger +* Identities owning the verifying enclave hosting infrastructure +* Identities owning the encrypted ledger/signature storage infrastructure +* R3 = enclave whitelisting identity +* Network Map = contract whitelisting identity +* Intel + +We have two major ways of compromise: + +* compromise of a non-enclave entity (notary, node, R3, Network Map, storage) +* compromise of an enclave. + +In the case of **notaries** compromise means malicious signatures, for **nodes** it's malicious transactions, for **R3** +it's signing malicious enclaves, for **Network Map** it's signing malicious contracts, for **storage** it's read-write +access to encrypted data, and for **Intel** it's forging of quotes or signing over invalid ones. + +A compromise of an **enclave** means some form of access to the enclave's temporary identity key. This may happen +through direct hardware compromise (extracting of fuse values) and subsequent forging of a quote, or leaking of secrets +through weakness of the enclave-host boundary or other side-channels like Spectre(hacking). In any case it allows an +adversary to impersonate an enclave and therefore to intercept enclave traffic and forge signatures. + +The actors relevant to SGX are enclave hosts, storage infrastructure owners, regular nodes and R3. + +* **Enclave hosts**: enclave code is specifically written with malicious (compromised) hosts in mind. That said we + cannot be 100% secure against yet undiscovered side channel attacks and other vulnerabilities, so we need to be + prepared for the scenario where enclaves get compromised. The privacy model effectively solves this problem by + always provisioning and re-verifying the backchain. An impersonated enclave may be able to see what's on the ledger, + but tampering with it will not check out at the next provisioning. On the other hand if a compromise happens in the + integrity model an attacker can forge a signature over validity. We can mitigate this with a BFT guarantee by + requiring a consensus over validity. This way we effectively provide the same guarantee for validity as notaries + provide with regards to double spend. + +* **Storage infrastructure owner**: + * A malicious actor would need to crack the encryption key to decrypt transactions + or transaction signatures. Although this is highly unlikely, we can mitigate by preparing for and forcing of key + updates (i.e. we won't provision new transactions to enclaves using old keys). + * What an attacker *can* do is simply erase encrypted data (or perhaps re-encrypt as part of ransomware), blocking + subsequent resolution and verification. In the non-notary provisioning models we can't really mitigate this as the + tip of the ledger (or signature over) may only be stored by a single non-notary entity (assumed to be compromised). + However if we require consensus over validity between notary or non-notary entities (e.g. validating oracles) then + this implicitly provides redundancy of storage. + * Furthermore storage owners can spy on the enclave's activity by observing access patterns to the encrypted blobs. + We can mitigate by implementing ORAM storage. + +* **Regular nodes**: if a regular node is compromised the attacker may gain access to the node's long term key that + allows them to Diffie-Hellman with an enclave, or get the ephemeral DH value calculated during attestation directly. + This means they can man-in-the-middle between the node and the enclave. From the ledger's point of view we are + prepared for this scenario as we never leak sensitive information to the node from the enclave, however it opens the + possibility that the attacker can fake enclave replies (e.g. validity checks) and can sniff on secrets flowing from + the node to the enclave. We can mitigate the fake enclave replies by requiring an extra signature on messages. + Sniffing cannot really be mitigated, but one could argue that if the transient DH key (that lives temporarily in + volatile memory) or long term key (that probably lives in an HSM) was leaked then the attacker has access to node + secrets anyway. + +* **R3**: the entity that's whitelisting enclaves effectively controls attestation trust, which means they can + backdoor the ledger by whitelisting a secret-revealing/signature-forging enclave. One way to mitigate this is by + requiring a threshold signature/consensus over new trusted enclave measurements. Another way would be to use "canary" + keys controlled by neutral parties. These parties' responsibility would simply be to publish enclave measurements (and + perhaps the reproducing build) to the public before signing over them. The "publicity" and signature would be checked + during attestation, so a quote with a non-public measurement would be rejected. Although this wouldn't prevent + backdoors (unless the parties also do auditing), it would make them public. + +* **Intel**: There are two ways a compromised Intel can interact with the ledger maliciously, both provide a backdoor. + * It can sign over invalid quotes. This can be mitigated by implementing our own attestation service. Intel told us + we'll be able to do this in the future (by downloading a set of certificates tied to CPU+CPUSVN combos that may be + used to check QE signatures). + * It can produce valid quotes without an enclave. This is due to the fact that they store one half of the SGX- + specific fuse values in order to validate quotes flexibly. One way to circumvent this would be to only use the + other half of the fuse values (the seal values) which they don't store (or so they claim). However this requires + our own "enrollment" process of CPUs where we replicate the provisioning process based off of seal values and + verify manually that the provisioning public key comes from the CPU. And even if we do this all we did was move + the requirement of trust from Intel to R3. + + Note however that even if an attacker compromises Intel and decides to backdoor they would need to connect to the + ledger participants in order to take advantage. The flow framework and the business network concept act as a form of + ACL on data that would make an Intel backdoor quite useless. + +## Summary + +As we can see we have a number of options here, all of them have advantages and disadvantages. + +#### Privacy + non-notary + +**Pros**: +* Closest to our current non-SGX model +* Strong guarantee of validity +* Flexible with respect to notary modes + +**Cons**: +* Regulatory problem about provisioning of ledger +* Relies on ledger participants to do validation checks +* No redundancy across ledger participants + +#### Privacy + notary + +**Pros**: +* Strong guarantee of validity +* Separation of concerns, allows lightweight ledger participants +* Redundancy across notary nodes + +**Cons**: +* Regulatory problem about provisioning of ledger + +#### Integrity + non-notary + +**Pros**: +* Efficient validity checks +* No storage of sensitive transaction body only signatures + +**Cons**: +* Enclave impersonation compromises ledger (unless consensus validation) +* Relies on ledger participants to do validation checks +* No redundancy across ledger participants + +#### Integrity + notary + +**Pros**: +* Efficient validity check +* No storage of sensitive transaction body only signatures +* Separation of concerns, allows lightweight ledger participants +* Redundancy across notary nodes + +**Cons**: +* Only BFT guarantee over validity +* Temporary storage of transaction in RAM may be against regulation + +Personally I'm strongly leaning towards an integrity model where SGX compromise is mitigated by a BFT consensus over validity (perhaps done by a validating oracle cluster). This would solve the regulatory problem, it would be efficient and the infrastructure would have a very clean separation of concerns between notary and non-notary nodes, allowing lighter-weight interaction with the ledger. diff --git a/docs/source/deterministic-modules.rst b/docs/source/deterministic-modules.rst index 435b75c7fc..f6327f1649 100644 --- a/docs/source/deterministic-modules.rst +++ b/docs/source/deterministic-modules.rst @@ -30,7 +30,7 @@ JDK 8 Corda Modules ``core-deterministic`` and ``serialization-deterministic`` are generated from Corda's ``core`` and ``serialization`` modules respectively using both `ProGuard `_ and Corda's ``JarFilter`` Gradle - plugin. Corda developers configure these tools by applying Corda's ``@Deterministic`` and ``@NonDeterministic`` + plugin. Corda developers configure these tools by applying Corda's ``@KeepForDJVM`` and ``@DeleteForDJVM`` annotations to elements of ``core`` and ``serialization`` as described `here `_. The build generates each of Corda's deterministic JARs in six steps: @@ -42,12 +42,12 @@ The build generates each of Corda's deterministic JARs in six steps: .. sourcecode:: groovy - keep '@interface net.corda.core.Deterministic { *; }' + keep '@interface net.corda.core.KeepForDJVM { *; }' .. ProGuard works by calculating how much code is reachable from given "entry points", and in our case these entry - points are the ``@Deterministic`` classes. The unreachable classes are then discarded by ProGuard's ``shrink`` + points are the ``@KeepForDJVM`` classes. The unreachable classes are then discarded by ProGuard's ``shrink`` option. #. The remaining classes may still contain non-deterministic code. However, there is no way of writing a ProGuard rule explicitly to discard anything. Consider the following class: @@ -55,10 +55,10 @@ The build generates each of Corda's deterministic JARs in six steps: .. sourcecode:: kotlin @CordaSerializable - @Deterministic + @KeepForDJVM data class UniqueIdentifier(val externalId: String?, val id: UUID) : Comparable { - @NonDeterministic constructor(externalId: String?) : this(externalId, UUID.randomUUID()) - @NonDeterministic constructor() : this(null) + @DeleteForDJVM constructor(externalId: String?) : this(externalId, UUID.randomUUID()) + @DeleteForDJVM constructor() : this(null) ... } @@ -67,9 +67,9 @@ The build generates each of Corda's deterministic JARs in six steps: While CorDapps will definitely need to handle ``UniqueIdentifier`` objects, both of the secondary constructors generate a new random ``UUID`` and so are non-deterministic. Hence the next "determinising" step is to pass the classes to the ``JarFilter`` tool, which strips out all of the elements which have been annotated as - ``@NonDeterministic`` and stubs out any functions annotated with ``@NonDeterministicStub``. (Stub functions that + ``@DeleteForDJVM`` and stubs out any functions annotated with ``@StubOutForDJVM``. (Stub functions that return a value will throw ``UnsupportedOperationException``, whereas ``void`` or ``Unit`` stubs will do nothing.) - #. After the ``@NonDeterministic`` elements have been filtered out, the classes are rescanned using ProGuard to remove + #. After the ``@DeleteForDJVM`` elements have been filtered out, the classes are rescanned using ProGuard to remove any more code that has now become unreachable. #. The remaining classes define our deterministic subset. However, the ``@kotlin.Metadata`` annotations on the compiled Kotlin classes still contain references to all of the functions and properties that ProGuard has deleted. Therefore @@ -77,26 +77,10 @@ The build generates each of Corda's deterministic JARs in six steps: deleted functions and properties are still present. #. Finally, we use ProGuard again to validate our JAR against the deterministic ``rt.jar``: - .. sourcecode:: groovy - - task checkDeterminism(type: ProGuardTask, dependsOn: jdkTask) { - injars metafix - - libraryjars "$deterministic_jdk_home/jre/lib/rt.jar" - - configurations.runtimeLibraries.forEach { - libraryjars it.path, filter: '!META-INF/versions/**' - } - - keepattributes '*' - dontpreverify - dontobfuscate - dontoptimize - verbose - - keep 'class *' - } - + .. literalinclude:: ../../core-deterministic/build.gradle + :language: groovy + :start-after: DOCSTART 01 + :end-before: DOCEND 01 .. This step will fail if ProGuard spots any Java API references that still cannot be satisfied by the deterministic @@ -123,7 +107,7 @@ The ``testing`` module also has two sub-modules: .. _deterministic_annotations: -Applying @Deterministic and @NonDeterministic annotations +Applying @KeepForDJVM and @DeleteForDJVM annotations --------------------------------------------------------- Corda developers need to understand how to annotate classes in the ``core`` and ``serialization`` modules correctly @@ -134,7 +118,7 @@ in order to maintain the deterministic JARs. .. sourcecode:: kotlin - @file:Deterministic + @file:KeepForDJVM package net.corda.core.internal .. @@ -146,14 +130,12 @@ For more information about how ``JarFilter`` is processing the byte-code inside use Gradle's ``--info`` or ``--debug`` command-line options. Deterministic Classes - Classes that *must* be included in the deterministic JAR should be annotated as ``@Deterministic``. + Classes that *must* be included in the deterministic JAR should be annotated as ``@KeepForDJVM``. - .. sourcecode:: kotlin - - @Target(FILE, CLASS) - @Retention(BINARY) - @CordaInternal - annotation class Deterministic + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/KeepForDJVM.kt + :language: kotlin + :start-after: DOCSTART 01 + :end-before: DOCEND 01 .. To preserve any Kotlin functions, properties or type aliases that have been declared outside of a ``class``, @@ -162,7 +144,7 @@ Deterministic Classes .. sourcecode:: kotlin @file:JvmName("InternalUtils") - @file:Deterministic + @file:KeepForDJVM package net.corda.core.internal infix fun Temporal.until(endExclusive: Temporal): Duration = Duration.between(this, endExclusive) @@ -170,25 +152,12 @@ Deterministic Classes .. Non-Deterministic Elements - Elements that *must* be deleted from classes in the deterministic JAR should be annotated as ``@NonDeterministic``. - - .. sourcecode:: kotlin - - @Target( - FILE, - CLASS, - CONSTRUCTOR, - FUNCTION, - PROPERTY_GETTER, - PROPERTY_SETTER, - PROPERTY, - FIELD, - TYPEALIAS - ) - @Retention(BINARY) - @CordaInternal - annotation class NonDeterministic + Elements that *must* be deleted from classes in the deterministic JAR should be annotated as ``@DeleteForDJVM``. + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/DeleteForDJVM.kt + :language: kotlin + :start-after: DOCSTART 01 + :end-before: DOCEND 01 .. You must also ensure that a deterministic class's primary constructor does not reference any classes that are @@ -206,32 +175,24 @@ Non-Deterministic Elements package net.corda.core - @NonDeterministic + @DeleteForDJVM val map: MutableMap = ConcurrentHashMap() .. In this case, ``JarFilter`` would delete the ``map`` property but the ```` block would still create an instance of ``ConcurrentHashMap``. The solution here is to refactor the property into its own file and then - annotate the file itself as ``@NonDeterministic`` instead. + annotate the file itself as ``@DeleteForDJVM`` instead. Non-Deterministic Function Stubs Sometimes it is impossible to delete a function entirely. Or a function may have some non-deterministic code - embedded inside it that cannot be removed. For these rare cases, there is the ``@NonDeterministicStub`` + embedded inside it that cannot be removed. For these rare cases, there is the ``@StubOutForDJVM`` annotation: - .. sourcecode:: kotlin - - @Target( - CONSTRUCTOR, - FUNCTION, - PROPERTY_GETTER, - PROPERTY_SETTER - ) - @Retention(BINARY) - @CordaInternal - annotation class NonDeterministicStub - + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/StubOutForDJVM.kt + :language: kotlin + :start-after: DOCSTART 01 + :end-before: DOCEND 01 .. This annotation instructs ``JarFilter`` to replace the function's body with either an empty body (for functions @@ -244,10 +205,9 @@ Non-Deterministic Function Stubs otherOperations() } - @NonDeterministicStub + @StubOutForDJVM private fun nonDeterministicOperations() { // etc } .. - diff --git a/docs/source/index.rst b/docs/source/index.rst index f2ce85fe13..358c5232b0 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -42,6 +42,7 @@ We look forward to seeing what you can do with Corda! node-internals-index.rst component-library-index.rst troubleshooting.rst + json.rst .. toctree:: :caption: Operations @@ -64,6 +65,7 @@ We look forward to seeing what you can do with Corda! design/hadr/design.md design/kafka-notary/design.md design/monitoring-management/design.md + design/sgx-integration/design.md .. toctree:: :caption: Participate @@ -73,4 +75,3 @@ We look forward to seeing what you can do with Corda! corda-repo-layout.rst deterministic-modules.rst building-the-docs.rst - json.rst diff --git a/jdk8u-deterministic/build.gradle b/jdk8u-deterministic/build.gradle new file mode 100644 index 0000000000..298953c30f --- /dev/null +++ b/jdk8u-deterministic/build.gradle @@ -0,0 +1,30 @@ +repositories { + maven { + url "$artifactory_contextUrl/corda-releases" + } + maven { + url "$artifactory_contextUrl/corda-dev" + } +} + +ext { + jdk_home = "$buildDir/jdk" +} + +configurations { + jdk +} + +dependencies { + jdk "net.corda:deterministic-rt:$deterministic_rt_version:api" +} + +task copyJdk(type: Copy) { + from(configurations.jdk.asPath) { + rename 'deterministic-rt-(.*).jar', 'rt.jar' + } + into "$jdk_home/jre/lib" +} + +assemble.dependsOn copyJdk +jar.enabled = false diff --git a/node/src/integration-test/kotlin/net/corda/node/services/events/ScheduledFlowIntegrationTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/events/ScheduledFlowIntegrationTests.kt index ddf20d700c..7f47122619 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/events/ScheduledFlowIntegrationTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/events/ScheduledFlowIntegrationTests.kt @@ -1,13 +1,3 @@ -/* - * R3 Proprietary and Confidential - * - * Copyright (c) 2018 R3 Limited. All rights reserved. - * - * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. - * - * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. - */ - package net.corda.node.services.events import co.paralleluniverse.fibers.Suspendable diff --git a/node/src/integration-test/kotlin/net/corda/testMessage/ScheduledState.kt b/node/src/integration-test/kotlin/net/corda/testMessage/ScheduledState.kt index cc5f14e8ee..189d0fc9f4 100644 --- a/node/src/integration-test/kotlin/net/corda/testMessage/ScheduledState.kt +++ b/node/src/integration-test/kotlin/net/corda/testMessage/ScheduledState.kt @@ -1,13 +1,3 @@ -/* - * R3 Proprietary and Confidential - * - * Copyright (c) 2018 R3 Limited. All rights reserved. - * - * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. - * - * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. - */ - package net.corda.testMessage import co.paralleluniverse.fibers.Suspendable diff --git a/node/src/main/kotlin/net/corda/node/serialization/kryo/Kryo.kt b/node/src/main/kotlin/net/corda/node/serialization/kryo/Kryo.kt index e1a8654f23..068713d05f 100644 --- a/node/src/main/kotlin/net/corda/node/serialization/kryo/Kryo.kt +++ b/node/src/main/kotlin/net/corda/node/serialization/kryo/Kryo.kt @@ -17,6 +17,7 @@ import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer import com.esotericsoftware.kryo.serializers.FieldSerializer import com.esotericsoftware.kryo.util.MapReferenceResolver +import net.corda.core.DeleteForDJVM import net.corda.core.contracts.PrivacySalt import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash @@ -474,6 +475,7 @@ fun Kryo.serializationContext(): SerializeAsTokenContext? = context.get(serializ * unmodifiable collection to [java.lang.Throwable.suppressedExceptions] which will fail some sentinel identity checks * e.g. in [java.lang.Throwable.addSuppressed] */ +@DeleteForDJVM @ThreadSafe class ThrowableSerializer(kryo: Kryo, type: Class) : Serializer(false, true) { diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 7cb92ed609..cf466a59d8 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -499,12 +499,21 @@ class NodeVaultService( return database.transaction { concurrentBox.exclusive { val snapshotResults = _queryBy(criteria, paging, sorting, contractStateType) - val updates: Observable> = uncheckedCast(_updatesPublisher.bufferUntilSubscribed().filter { it.containsType(contractStateType, snapshotResults.stateTypes) }) + val updates: Observable> = uncheckedCast(_updatesPublisher.bufferUntilSubscribed() + .filter { it.containsType(contractStateType, snapshotResults.stateTypes) } + .map { filterContractStates(it, contractStateType) }) DataFeed(snapshotResults, updates) } } } + private fun filterContractStates(update: Vault.Update, contractStateType: Class) = + update.copy(consumed = filterByContractState(contractStateType, update.consumed), + produced = filterByContractState(contractStateType, update.produced)) + + private fun filterByContractState(contractStateType: Class, stateAndRefs: Set>) = + stateAndRefs.filter { contractStateType.isAssignableFrom(it.state.data.javaClass) }.toSet() + private fun getSession() = database.currentOrNew().session /** * Derive list from existing vault states and then incrementally update using vault observables diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 133250100a..63000115b2 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -38,10 +38,7 @@ import net.corda.nodeapi.internal.persistence.DatabaseTransaction import net.corda.testing.core.* import net.corda.testing.internal.TEST_TX_TIME import net.corda.testing.internal.rigorousMock -import net.corda.testing.internal.vault.DUMMY_LINEAR_CONTRACT_PROGRAM_ID -import net.corda.testing.internal.vault.DummyLinearContract -import net.corda.testing.internal.vault.DummyLinearStateSchemaV1 -import net.corda.testing.internal.vault.VaultFiller +import net.corda.testing.internal.vault.* import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices import net.corda.testing.node.makeTestIdentityService @@ -2293,4 +2290,73 @@ class VaultQueryTests : VaultQueryTestsBase(), VaultQueryParties by delegate { ) } } + + @Test + fun `track by only returns updates of tracked type`() { + val updates = database.transaction { + val (snapshot, updates) = vaultService.trackBy() + assertThat(snapshot.states).hasSize(0) + val states = vaultFiller.fillWithSomeTestLinearAndDealStates(10).states + this.session.flush() + vaultFiller.consumeLinearStates(states.toList()) + updates + } + + updates.expectEvents { + sequence( + expect { (consumed, produced, flowId) -> + require(flowId == null) {} + require(consumed.isEmpty()) {} + require(produced.size == 10) {} + require(produced.filter { DummyDealContract.State::class.java.isAssignableFrom(it.state.data::class.java) }.size == 10) {} + } + ) + } + } + + @Test + fun `track by of super class only returns updates of sub classes of tracked type`() { + val updates = database.transaction { + val (snapshot, updates) = vaultService.trackBy() + assertThat(snapshot.states).hasSize(0) + val states = vaultFiller.fillWithSomeTestLinearAndDealStates(10).states + this.session.flush() + vaultFiller.consumeLinearStates(states.toList()) + updates + } + + updates.expectEvents { + sequence( + expect { (consumed, produced, flowId) -> + require(flowId == null) {} + require(consumed.isEmpty()) {} + require(produced.size == 10) {} + require(produced.filter { DealState::class.java.isAssignableFrom(it.state.data::class.java) }.size == 10) {} + } + ) + } + } + + @Test + fun `track by of contract state interface returns updates of all states`() { + val updates = database.transaction { + val (snapshot, updates) = vaultService.trackBy() + assertThat(snapshot.states).hasSize(0) + val states = vaultFiller.fillWithSomeTestLinearAndDealStates(10).states + this.session.flush() + vaultFiller.consumeLinearStates(states.toList()) + updates + } + + updates.expectEvents { + sequence( + expect { (consumed, produced, flowId) -> + require(flowId == null) {} + require(consumed.isEmpty()) {} + require(produced.size == 20) {} + require(produced.filter { ContractState::class.java.isAssignableFrom(it.state.data::class.java) }.size == 20) {} + } + ) + } + } } diff --git a/serialization-deterministic/build.gradle b/serialization-deterministic/build.gradle new file mode 100644 index 0000000000..3893c56584 --- /dev/null +++ b/serialization-deterministic/build.gradle @@ -0,0 +1,195 @@ +/* + * R3 Proprietary and Confidential + * + * Copyright (c) 2018 R3 Limited. All rights reserved. + * + * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. + * + * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. + */ +description 'Corda serialization (deterministic)' + +apply plugin: 'kotlin' +apply plugin: 'com.jfrog.artifactory' +apply plugin: 'net.corda.plugins.publish-utils' + +evaluationDependsOn(':jdk8u-deterministic') +evaluationDependsOn(":serialization") + +def javaHome = System.getProperty('java.home') +def jarBaseName = "corda-${project.name}".toString() +def jdkTask = project(':jdk8u-deterministic').assemble +def deterministic_jdk_home = project(':jdk8u-deterministic').jdk_home + +configurations { + runtimeLibraries + runtimeArtifacts.extendsFrom runtimeLibraries +} + +dependencies { + compileOnly project(':serialization') + compileOnly "$quasar_group:quasar-core:$quasar_version:jdk8" + + // Configure these by hand. It should be a minimal subset of dependencies, + // and without any obviously non-deterministic ones such as Hibernate. + runtimeLibraries project(path: ':core-deterministic', configuration: 'runtimeArtifacts') + runtimeLibraries "io.github.lukehutch:fast-classpath-scanner:$fast_classpath_scanner_version" + runtimeLibraries "org.apache.qpid:proton-j:$protonj_version" + runtimeLibraries "org.iq80.snappy:snappy:$snappy_version" +} + +tasks.withType(AbstractCompile) { + dependsOn jdkTask +} + +tasks.withType(JavaCompile) { + options.compilerArgs << "-bootclasspath" << "$deterministic_jdk_home/jre/lib/rt.jar".toString() +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) { + kotlinOptions.jdkHome = deterministic_jdk_home +} + +jar { + baseName 'DOES-NOT-EXIST' + // Don't build a jar here because it would be the wrong one. + // The jar we really want will be built by the metafix task. + enabled = false +} + +def serializationJarTask = tasks.getByPath(':serialization:jar') +def originalJar = serializationJarTask.outputs.files.singleFile + +task patchSerialization(type: Zip, dependsOn: serializationJarTask) { + destinationDir file("$buildDir/source-libs") + metadataCharset 'UTF-8' + classifier 'transient' + extension 'jar' + + from(compileKotlin) + from(zipTree(originalJar)) { + exclude 'net/corda/serialization/internal/AttachmentsClassLoaderBuilder*' + exclude 'net/corda/serialization/internal/ByteBufferStreams*' + exclude 'net/corda/serialization/internal/DefaultWhitelist*' + exclude 'net/corda/serialization/internal/amqp/AMQPSerializerFactories*' + exclude 'net/corda/serialization/internal/amqp/AMQPStreams*' + } + + reproducibleFileOrder = true + includeEmptyDirs = false +} + +import proguard.gradle.ProGuardTask +task predeterminise(type: ProGuardTask, dependsOn: project(':core-deterministic').assemble) { + injars patchSerialization + outjars "$buildDir/proguard/pre-deterministic-${project.version}.jar" + + libraryjars "$javaHome/lib/rt.jar" + libraryjars "$javaHome/lib/jce.jar" + libraryjars "$javaHome/lib/ext/sunec.jar" + configurations.compileOnly.forEach { + if (originalJar.path != it.path) { + libraryjars it.path, filter: '!META-INF/versions/**' + } + } + + keepattributes '*' + keepdirectories + dontpreverify + dontobfuscate + dontoptimize + printseeds + verbose + + keep '@net.corda.core.KeepForDJVM class * { *; }', includedescriptorclasses:true + keepclassmembers 'class net.corda.serialization.** { public synthetic ; }' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars predeterminise + annotations { + forDelete = [ + "net.corda.core.DeleteForDJVM" + ] + forStub = [ + "net.corda.core.StubOutForDJVM" + ] + forRemove = [ + "co.paralleluniverse.fibers.Suspendable" + ] + } +} + +task determinise(type: ProGuardTask) { + injars jarFilter + outjars "$buildDir/proguard/$jarBaseName-${project.version}.jar" + + libraryjars "$javaHome/lib/rt.jar" + libraryjars "$javaHome/lib/jce.jar" + configurations.runtimeLibraries.forEach { + libraryjars it.path, filter: '!META-INF/versions/**' + } + + // Analyse the JAR for dead code, and remove (some of) it. + optimizations 'code/removal/simple,code/removal/advanced' + printconfiguration + + keepattributes '*' + keepdirectories + dontobfuscate + printseeds + verbose + + keep '@net.corda.core.KeepForDJVM class * { *; }', includedescriptorclasses:true + keepclassmembers 'class net.corda.serialization.** { public synthetic ; }' +} + +import net.corda.gradle.jarfilter.MetaFixerTask +task metafix(type: MetaFixerTask) { + outputDir file("$buildDir/libs") + jars determinise + suffix "" + + // Strip timestamps from the JAR to make it reproducible. + preserveTimestamps = false +} + +task checkDeterminism(type: ProGuardTask, dependsOn: jdkTask) { + injars metafix + + libraryjars "$deterministic_jdk_home/jre/lib/rt.jar" + + configurations.runtimeLibraries.forEach { + libraryjars it.path, filter: '!META-INF/versions/**' + } + + keepattributes '*' + dontpreverify + dontobfuscate + dontoptimize + verbose + + keep 'class *' +} + +defaultTasks "determinise" +determinise.finalizedBy metafix +metafix.finalizedBy checkDeterminism +assemble.dependsOn checkDeterminism + +def deterministicJar = metafix.outputs.files.singleFile +artifacts { + runtimeArtifacts file: deterministicJar, name: jarBaseName, type: 'jar', extension: 'jar', builtBy: metafix + publish file: deterministicJar, name: jarBaseName, type: 'jar', extension: 'jar', builtBy: metafix +} + +publish { + dependenciesFrom configurations.runtimeArtifacts + publishSources = false + publishJavadoc = false + name jarBaseName +} + +// Must be after publish {} so that the previous install task exists for overwriting. +task install(overwrite: true, dependsOn: 'publishToMavenLocal') diff --git a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/AttachmentsClassLoaderBuilder.kt b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/AttachmentsClassLoaderBuilder.kt new file mode 100644 index 0000000000..01386d8823 --- /dev/null +++ b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/AttachmentsClassLoaderBuilder.kt @@ -0,0 +1,13 @@ +package net.corda.serialization.internal + +import net.corda.core.crypto.SecureHash +import java.lang.ClassLoader + +/** + * Drop-in replacement for [AttachmentsClassLoaderBuilder] in the serialization module. + * This version is not strongly-coupled to [net.corda.core.node.ServiceHub]. + */ +@Suppress("UNUSED", "UNUSED_PARAMETER") +internal class AttachmentsClassLoaderBuilder(private val properties: Map, private val deserializationClassLoader: ClassLoader) { + fun build(attachmentHashes: List): AttachmentsClassLoader? = null +} \ No newline at end of file diff --git a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/ByteBufferStreams.kt b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/ByteBufferStreams.kt new file mode 100644 index 0000000000..4a93bcb5f8 --- /dev/null +++ b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/ByteBufferStreams.kt @@ -0,0 +1,15 @@ +@file:JvmName("ByteBufferStreams") +package net.corda.serialization.internal + +const val DEFAULT_BYTEBUFFER_SIZE = 64 * 1024 + +/** + * Drop-in replacement for [byteArrayOutput] in the serialization module. + * This version does not use a [net.corda.core.internal.LazyPool]. + */ +internal fun byteArrayOutput(task: (ByteBufferOutputStream) -> T): ByteArray { + return ByteBufferOutputStream(DEFAULT_BYTEBUFFER_SIZE).let { underlying -> + task(underlying) + underlying.toByteArray() // Must happen after close, to allow ZIP footer to be written for example. + } +} diff --git a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/DefaultWhitelist.kt b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/DefaultWhitelist.kt new file mode 100644 index 0000000000..b1435f6c2f --- /dev/null +++ b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/DefaultWhitelist.kt @@ -0,0 +1,10 @@ +package net.corda.serialization.internal + +import net.corda.core.serialization.SerializationWhitelist + +/** + * The DJVM does not need whitelisting, by definition. + */ +object DefaultWhitelist : SerializationWhitelist { + override val whitelist: List> get() = emptyList() +} diff --git a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt new file mode 100644 index 0000000000..8e1d0465c1 --- /dev/null +++ b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt @@ -0,0 +1,34 @@ +@file:JvmName("AMQPSerializerFactories") +package net.corda.serialization.internal.amqp + +import net.corda.core.serialization.ClassWhitelist +import net.corda.core.serialization.SerializationContext +import net.corda.serialization.internal.carpenter.ClassCarpenter +import net.corda.serialization.internal.carpenter.Schema + +/** + * Creates a [SerializerFactoryFactory] suitable for the DJVM, + * i.e. one without a [ClassCarpenter] implementation. + */ +@Suppress("UNUSED") +fun createSerializerFactoryFactory(): SerializerFactoryFactory = DeterministicSerializerFactoryFactory() + +private class DeterministicSerializerFactoryFactory : SerializerFactoryFactory { + override fun make(context: SerializationContext) = + SerializerFactory( + whitelist = context.whitelist, + classCarpenter = DummyClassCarpenter(context.whitelist, context.deserializationClassLoader), + serializersByType = mutableMapOf(), + serializersByDescriptor = mutableMapOf(), + customSerializers = ArrayList(), + transformsCache = mutableMapOf() + ) +} + +private class DummyClassCarpenter( + override val whitelist: ClassWhitelist, + override val classloader: ClassLoader +) : ClassCarpenter { + override fun build(schema: Schema): Class<*> + = throw UnsupportedOperationException("ClassCarpentry not supported") +} \ No newline at end of file diff --git a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPStreams.kt b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPStreams.kt new file mode 100644 index 0000000000..1f4d9af591 --- /dev/null +++ b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPStreams.kt @@ -0,0 +1,40 @@ +@file:JvmName("AMQPStreams") +package net.corda.serialization.internal.amqp + +import net.corda.serialization.internal.ByteBufferInputStream +import net.corda.serialization.internal.ByteBufferOutputStream +import net.corda.serialization.internal.DEFAULT_BYTEBUFFER_SIZE +import java.io.InputStream +import java.io.OutputStream +import java.nio.ByteBuffer + +/** + * Drop-in replacement for [InputStream.asByteBuffer] in the serialization module. + * This version does not use a [net.corda.core.internal.LazyPool]. + */ +fun InputStream.asByteBuffer(): ByteBuffer { + return if (this is ByteBufferInputStream) { + byteBuffer // BBIS has no other state, so this is perfectly safe. + } else { + ByteBuffer.wrap(ByteBufferOutputStream(DEFAULT_BYTEBUFFER_SIZE).let { + copyTo(it) + it.toByteArray() + }) + } +} + +/** + * Drop-in replacement for [OutputStream.alsoAsByteBuffer] in the serialization module. + * This version does not use a [net.corda.core.internal.LazyPool]. + */ +fun OutputStream.alsoAsByteBuffer(remaining: Int, task: (ByteBuffer) -> T): T { + return if (this is ByteBufferOutputStream) { + alsoAsByteBuffer(remaining, task) + } else { + ByteBufferOutputStream(DEFAULT_BYTEBUFFER_SIZE).let { + val result = it.alsoAsByteBuffer(remaining, task) + it.copyTo(this) + result + } + } +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/AllButBlacklisted.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/AllButBlacklisted.kt index ad1860ee37..0e085dfb68 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/AllButBlacklisted.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/AllButBlacklisted.kt @@ -10,6 +10,7 @@ package net.corda.serialization.internal +import net.corda.core.DeleteForDJVM import net.corda.core.serialization.ClassWhitelist import sun.misc.Unsafe import sun.security.util.Password @@ -44,6 +45,7 @@ import kotlin.collections.LinkedHashSet * in the blacklist - it will still be serialized as specified by custom serializer. * For more details, see [net.corda.serialization.internal.CordaClassResolver.getRegistration] */ +@DeleteForDJVM object AllButBlacklisted : ClassWhitelist { private val blacklistedClasses = hashSetOf( diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/AttachmentsClassLoader.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/AttachmentsClassLoader.kt index 087e0668ea..da97444428 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/AttachmentsClassLoader.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/AttachmentsClassLoader.kt @@ -10,6 +10,7 @@ package net.corda.serialization.internal +import net.corda.core.KeepForDJVM import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractAttachment import net.corda.core.crypto.SecureHash @@ -32,6 +33,7 @@ import java.util.* * AttachmentsClassLoader is somewhat expensive, as every attachment is scanned to ensure that there are no overlapping * file paths. */ +@KeepForDJVM class AttachmentsClassLoader(attachments: List, parent: ClassLoader = ClassLoader.getSystemClassLoader()) : SecureClassLoader(parent) { private val pathsToAttachments = HashMap() private val idsToAttachments = HashMap() diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/ByteBufferStreams.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/ByteBufferStreams.kt index a37ced49e2..144a0bb047 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/ByteBufferStreams.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/ByteBufferStreams.kt @@ -1,7 +1,9 @@ @file:JvmName("ByteBufferStreams") - +@file:DeleteForDJVM package net.corda.serialization.internal +import net.corda.core.DeleteForDJVM +import net.corda.core.KeepForDJVM import net.corda.core.internal.LazyPool import java.io.ByteArrayOutputStream import java.io.IOException @@ -22,6 +24,7 @@ fun byteArrayOutput(task: (ByteBufferOutputStream) -> T): ByteArray { } } +@KeepForDJVM class ByteBufferInputStream(val byteBuffer: ByteBuffer) : InputStream() { @Throws(IOException::class) override fun read(): Int { @@ -43,6 +46,7 @@ class ByteBufferInputStream(val byteBuffer: ByteBuffer) : InputStream() { } } +@KeepForDJVM class ByteBufferOutputStream(size: Int) : ByteArrayOutputStream(size) { companion object { private val ensureCapacity = ByteArrayOutputStream::class.java.getDeclaredMethod("ensureCapacity", Int::class.java).apply { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/ClassWhitelists.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/ClassWhitelists.kt index 5d647d4f17..a99eba253e 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/ClassWhitelists.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/ClassWhitelists.kt @@ -1,16 +1,20 @@ package net.corda.serialization.internal +import net.corda.core.KeepForDJVM import net.corda.core.serialization.ClassWhitelist import java.util.* +@KeepForDJVM interface MutableClassWhitelist : ClassWhitelist { fun add(entry: Class<*>) } +@KeepForDJVM object AllWhitelist : ClassWhitelist { override fun hasListed(type: Class<*>): Boolean = true } +@KeepForDJVM class BuiltInExceptionsWhitelist : ClassWhitelist { companion object { private val packageName = "^(?:java|kotlin)(?:[.]|$)".toRegex() @@ -40,9 +44,11 @@ sealed class AbstractMutableClassWhitelist(private val whitelist: MutableSet = Collections.synchronizedSet(mutableSetOf()) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/ClientContexts.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/ClientContexts.kt index 1fafa7ad29..06dbf7320a 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/ClientContexts.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/ClientContexts.kt @@ -8,10 +8,11 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ +@file:DeleteForDJVM @file:JvmName("ClientContexts") - package net.corda.serialization.internal +import net.corda.core.DeleteForDJVM import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationDefaults import net.corda.serialization.internal.amqp.amqpMagic diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/GeneratedAttachment.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/GeneratedAttachment.kt index 3786ddc33f..8f6f380fe4 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/GeneratedAttachment.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/GeneratedAttachment.kt @@ -10,9 +10,11 @@ package net.corda.serialization.internal +import net.corda.core.KeepForDJVM import net.corda.core.crypto.sha256 import net.corda.core.internal.AbstractAttachment +@KeepForDJVM class GeneratedAttachment(val bytes: ByteArray) : AbstractAttachment({ bytes }) { override val id = bytes.sha256() } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/OrdinalIO.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/OrdinalIO.kt index fd92d31fbc..c582784e28 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/OrdinalIO.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/OrdinalIO.kt @@ -10,11 +10,13 @@ package net.corda.serialization.internal +import net.corda.core.KeepForDJVM import java.io.EOFException import java.io.InputStream import java.io.OutputStream import java.nio.ByteBuffer +@KeepForDJVM class OrdinalBits(private val ordinal: Int) { interface OrdinalWriter { val bits: OrdinalBits @@ -29,6 +31,7 @@ class OrdinalBits(private val ordinal: Int) { } } +@KeepForDJVM class OrdinalReader(private val values: Array) { private val enumName = values[0].javaClass.simpleName private val range = 0 until values.size diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationFormat.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationFormat.kt index bb9de04eaf..cb70b8053a 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationFormat.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationFormat.kt @@ -10,6 +10,7 @@ package net.corda.serialization.internal +import net.corda.core.KeepForDJVM import net.corda.core.serialization.SerializationEncoding import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.OpaqueBytes @@ -22,6 +23,7 @@ import java.nio.ByteBuffer import java.util.zip.DeflaterOutputStream import java.util.zip.InflaterInputStream +@KeepForDJVM class CordaSerializationMagic(bytes: ByteArray) : OpaqueBytes(bytes) { private val bufferView = slice() fun consume(data: ByteSequence): ByteBuffer? { @@ -29,6 +31,7 @@ class CordaSerializationMagic(bytes: ByteArray) : OpaqueBytes(bytes) { } } +@KeepForDJVM enum class SectionId : OrdinalWriter { /** Serialization data follows, and then discard the rest of the stream (if any) as legacy data may have trailing garbage. */ DATA_AND_STOP, @@ -44,6 +47,7 @@ enum class SectionId : OrdinalWriter { override val bits = OrdinalBits(ordinal) } +@KeepForDJVM enum class CordaSerializationEncoding : SerializationEncoding, OrdinalWriter { DEFLATE { override fun wrap(stream: OutputStream) = DeflaterOutputStream(stream) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt index fe9482296d..5c3ab76fc4 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt @@ -12,6 +12,8 @@ package net.corda.serialization.internal import com.github.benmanes.caffeine.cache.Cache import com.github.benmanes.caffeine.cache.Caffeine +import net.corda.core.DeleteForDJVM +import net.corda.core.KeepForDJVM import net.corda.core.contracts.Attachment import net.corda.core.crypto.SecureHash import net.corda.core.internal.copyBytes @@ -30,6 +32,7 @@ internal object NullEncodingWhitelist : EncodingWhitelist { override fun acceptEncoding(encoding: SerializationEncoding) = false } +@KeepForDJVM data class SerializationContextImpl @JvmOverloads constructor(override val preferredSerializationVersion: SerializationMagic, override val deserializationClassLoader: ClassLoader, override val whitelist: ClassWhitelist, @@ -74,9 +77,10 @@ data class SerializationContextImpl @JvmOverloads constructor(override val prefe } /* - * This class is internal rather than private so that node-api-deterministic + * This class is internal rather than private so that serialization-deterministic * can replace it with an alternative version. */ +@DeleteForDJVM internal class AttachmentsClassLoaderBuilder(private val properties: Map, private val deserializationClassLoader: ClassLoader) { private val cache: Cache, AttachmentsClassLoader> = Caffeine.newBuilder().weakValues().maximumSize(1024).build() @@ -100,10 +104,12 @@ internal class AttachmentsClassLoaderBuilder(private val properties: Map, SerializationScheme> ) : SerializationFactory() { + @DeleteForDJVM constructor() : this(ConcurrentHashMap()) companion object { @@ -165,6 +171,7 @@ open class SerializationFactoryImpl( } +@KeepForDJVM interface SerializationScheme { fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean @Throws(NotSerializableException::class) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializeAsTokenContextImpl.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializeAsTokenContextImpl.kt index 2e2a42d0a2..11ed59a31c 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializeAsTokenContextImpl.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializeAsTokenContextImpl.kt @@ -8,8 +8,10 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ +@file:DeleteForDJVM package net.corda.serialization.internal +import net.corda.core.DeleteForDJVM import net.corda.core.node.ServiceHub import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationFactory @@ -29,6 +31,7 @@ fun SerializationContext.withTokenContext(serializationContext: SerializeAsToken * Then it is a case of using the companion object methods on [SerializeAsTokenSerializer] to set and clear context as necessary * when serializing to enable/disable tokenization. */ +@DeleteForDJVM class SerializeAsTokenContextImpl(override val serviceHub: ServiceHub, init: SerializeAsTokenContext.() -> Unit) : SerializeAsTokenContext { constructor(toBeTokenized: Any, serializationFactory: SerializationFactory, context: SerializationContext, serviceHub: ServiceHub) : this(serviceHub, { serializationFactory.serialize(toBeTokenized, context.withTokenContext(this)) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/ServerContexts.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/ServerContexts.kt index 1e0e056ddb..4f6c1fc17d 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/ServerContexts.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/ServerContexts.kt @@ -9,9 +9,10 @@ */ @file:JvmName("ServerContexts") - +@file:DeleteForDJVM package net.corda.serialization.internal +import net.corda.core.DeleteForDJVM import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationDefaults import net.corda.serialization.internal.amqp.amqpMagic diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/SharedContexts.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/SharedContexts.kt index 87fde07b73..92f708d430 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/SharedContexts.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/SharedContexts.kt @@ -9,9 +9,11 @@ */ @file:JvmName("SharedContexts") - +@file:DeleteForDJVM package net.corda.serialization.internal +import net.corda.core.DeleteForDJVM +import net.corda.core.KeepForDJVM import net.corda.core.serialization.* import net.corda.serialization.internal.CordaSerializationEncoding.SNAPPY import net.corda.serialization.internal.amqp.amqpMagic @@ -26,6 +28,7 @@ val AMQP_P2P_CONTEXT = SerializationContextImpl( null ) +@KeepForDJVM object AlwaysAcceptEncodingWhitelist : EncodingWhitelist { override fun acceptEncoding(encoding: SerializationEncoding) = true } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/UseCaseAwareness.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/UseCaseAwareness.kt index 43f64d3823..4aa03356b9 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/UseCaseAwareness.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/UseCaseAwareness.kt @@ -8,8 +8,10 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ +@file:KeepForDJVM package net.corda.serialization.internal +import net.corda.core.KeepForDJVM import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationFactory import java.util.* diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt index 21c6cba0f4..ce19935850 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt @@ -13,6 +13,9 @@ package net.corda.serialization.internal.amqp import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner +import net.corda.core.DeleteForDJVM +import net.corda.core.KeepForDJVM +import net.corda.core.StubOutForDJVM import net.corda.core.cordapp.Cordapp import net.corda.core.internal.objectOrNewInstance import net.corda.core.internal.uncheckedCast @@ -41,11 +44,13 @@ interface SerializerFactoryFactory { fun make(context: SerializationContext): SerializerFactory } +@KeepForDJVM abstract class AbstractAMQPSerializationScheme( private val cordappCustomSerializers: Set>, private val serializerFactoriesForContexts: MutableMap, SerializerFactory>, val sff: SerializerFactoryFactory = createSerializerFactoryFactory() ) : SerializationScheme { + @DeleteForDJVM constructor(cordapps: List) : this(cordapps.customSerializers, ConcurrentHashMap()) // TODO: This method of initialisation for the Whitelist and plugin serializers will have to change @@ -73,6 +78,7 @@ abstract class AbstractAMQPSerializationScheme( } } + @DeleteForDJVM val List.customSerializers get() = flatMap { it.serializationCustomSerializers }.toSet() } @@ -136,6 +142,7 @@ abstract class AbstractAMQPSerializationScheme( /* * Register the serializers which will be excluded from the DJVM. */ + @StubOutForDJVM private fun registerNonDeterministicSerializers(factory: SerializerFactory) { with(factory) { register(net.corda.serialization.internal.amqp.custom.SimpleStringSerializer) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializer.kt index 9123671774..1f4ef58b6c 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializer.kt @@ -10,6 +10,7 @@ package net.corda.serialization.internal.amqp +import net.corda.core.KeepForDJVM import net.corda.core.serialization.SerializationContext import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.codec.Data @@ -18,6 +19,7 @@ import java.lang.reflect.Type /** * Implemented to serialize and deserialize different types of objects to/from AMQP. */ +@KeepForDJVM interface AMQPSerializer { /** * The JVM type this can serialize and deserialize. diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPStreams.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPStreams.kt index c1d87d513a..a31096ecac 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPStreams.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPStreams.kt @@ -9,9 +9,10 @@ */ @file:JvmName("AMQPStreams") - +@file:DeleteForDJVM package net.corda.serialization.internal.amqp +import net.corda.core.DeleteForDJVM import net.corda.serialization.internal.ByteBufferInputStream import net.corda.serialization.internal.ByteBufferOutputStream import net.corda.serialization.internal.serializeOutputStreamPool diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt index 7e3132c3b8..3b10dcd638 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt @@ -10,6 +10,7 @@ package net.corda.serialization.internal.amqp +import net.corda.core.KeepForDJVM import net.corda.core.serialization.SerializationContext import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug @@ -23,6 +24,7 @@ import java.lang.reflect.Type /** * Serialization / deserialization of arrays. */ +@KeepForDJVM open class ArraySerializer(override val type: Type, factory: SerializerFactory) : AMQPSerializer { companion object { fun make(type: Type, factory: SerializerFactory) : AMQPSerializer { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CollectionSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CollectionSerializer.kt index 098d642c61..46fc05271e 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CollectionSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CollectionSerializer.kt @@ -10,6 +10,7 @@ package net.corda.serialization.internal.amqp +import net.corda.core.KeepForDJVM import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.SerializationContext import net.corda.core.utilities.NonEmptySet @@ -24,6 +25,7 @@ import kotlin.collections.LinkedHashSet /** * Serialization / deserialization of predefined set of supported [Collection] types covering mostly [List]s and [Set]s. */ +@KeepForDJVM class CollectionSerializer(private val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer { override val type: Type = declaredType as? DeserializedParameterizedType ?: DeserializedParameterizedType.make(SerializerFactory.nameForType(declaredType)) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt index e2be528590..5ced730276 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt @@ -10,6 +10,7 @@ package net.corda.serialization.internal.amqp +import net.corda.core.KeepForDJVM import net.corda.core.internal.VisibleForTesting import net.corda.core.serialization.EncodingWhitelist import net.corda.core.serialization.SerializationContext @@ -37,6 +38,7 @@ data class ObjectAndEnvelope(val obj: T, val envelope: Envelope) * @param serializerFactory This is the factory for [AMQPSerializer] instances and can be shared across multiple * instances and threads. */ +@KeepForDJVM class DeserializationInput @JvmOverloads constructor(private val serializerFactory: SerializerFactory, private val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist) { private val objectHistory: MutableList = mutableListOf() diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializedParameterizedType.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializedParameterizedType.kt index b17f44398d..643d6ab453 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializedParameterizedType.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializedParameterizedType.kt @@ -11,6 +11,7 @@ package net.corda.serialization.internal.amqp import com.google.common.primitives.Primitives +import net.corda.core.KeepForDJVM import java.io.NotSerializableException import java.lang.reflect.ParameterizedType import java.lang.reflect.Type @@ -21,6 +22,7 @@ import java.util.* * Implementation of [ParameterizedType] that we can actually construct, and a parser from the string representation * of the JDK implementation which we use as the textual format in the AMQP schema. */ +@KeepForDJVM class DeserializedParameterizedType(private val rawType: Class<*>, private val params: Array, private val ownerType: Type? = null) : ParameterizedType { init { if (params.isEmpty()) { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Envelope.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Envelope.kt index da36c69e6c..fe7541ca5a 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Envelope.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Envelope.kt @@ -10,6 +10,7 @@ package net.corda.serialization.internal.amqp +import net.corda.core.KeepForDJVM import org.apache.qpid.proton.amqp.DescribedType import org.apache.qpid.proton.codec.Data import org.apache.qpid.proton.codec.DescribedTypeConstructor @@ -23,6 +24,7 @@ import java.io.NotSerializableException */ // TODO: make the schema parsing lazy since mostly schemas will have been seen before and we only need it if we // TODO: don't recognise a type descriptor. +@KeepForDJVM data class Envelope(val obj: Any?, val schema: Schema, val transformsSchema: TransformsSchema) : DescribedType { companion object : DescribedTypeConstructor { val DESCRIPTOR = AMQPDescriptorRegistry.ENVELOPE.amqpDescriptor diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt index 3e10251372..31918980d3 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt @@ -10,6 +10,7 @@ package net.corda.serialization.internal.amqp +import net.corda.core.KeepForDJVM import net.corda.core.internal.isConcreteClass import net.corda.core.serialization.DeprecatedConstructorForDeserialization import net.corda.core.serialization.SerializationContext @@ -53,6 +54,7 @@ abstract class EvolutionSerializer( * should be placed * @param property object to read the actual property value */ + @KeepForDJVM data class OldParam(var resultsIndex: Int, val property: PropertySerializer) { fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput, new: Array, context: SerializationContext @@ -269,6 +271,7 @@ abstract class EvolutionSerializerGetterBase { * The normal use case for generating an [EvolutionSerializer]'s based on the differences * between the received schema and the class as it exists now on the class path, */ +@KeepForDJVM class EvolutionSerializerGetter : EvolutionSerializerGetterBase() { override fun getEvolutionSerializer(factory: SerializerFactory, typeNotation: TypeNotation, diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt index 6f61d18768..ebac797dda 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt @@ -12,6 +12,7 @@ package net.corda.serialization.internal.amqp import com.google.common.hash.Hasher import com.google.common.hash.Hashing +import net.corda.core.KeepForDJVM import net.corda.core.internal.kotlinObjectInstance import net.corda.core.utilities.loggerFor import net.corda.core.utilities.toBase64 @@ -22,6 +23,7 @@ import java.util.* /** * Should be implemented by classes which wish to provide plugable fingerprinting og types for a [SerializerFactory] */ +@KeepForDJVM interface FingerPrinter { /** * Return a unique identifier for a type, usually this will take into account the constituent elements @@ -38,6 +40,7 @@ interface FingerPrinter { /** * Implementation of the finger printing mechanism used by default */ +@KeepForDJVM class SerializerFingerPrinter : FingerPrinter { private var factory: SerializerFactory? = null diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/MapSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/MapSerializer.kt index c5236e2382..586b16beb4 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/MapSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/MapSerializer.kt @@ -10,6 +10,8 @@ package net.corda.serialization.internal.amqp +import net.corda.core.KeepForDJVM +import net.corda.core.StubOutForDJVM import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.SerializationContext import org.apache.qpid.proton.amqp.Symbol @@ -25,6 +27,7 @@ private typealias MapCreationFunction = (Map<*, *>) -> Map<*, *> /** * Serialization / deserialization of certain supported [Map] types. */ +@KeepForDJVM class MapSerializer(private val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer { override val type: Type = (declaredType as? DeserializedParameterizedType) ?: DeserializedParameterizedType.make(SerializerFactory.nameForType(declaredType), factory.classloader) @@ -140,6 +143,7 @@ private fun Class<*>.checkHashMap() { * The [WeakHashMap] class does not exist within the DJVM, and so we need * to isolate this reference. */ +@StubOutForDJVM private fun Class<*>.checkWeakHashMap() { if (WeakHashMap::class.java.isAssignableFrom(this)) { throw IllegalArgumentException("Weak references with map types not supported. Suggested fix: " diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializer.kt index 664ab1d1ad..8d34efe137 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializer.kt @@ -10,6 +10,7 @@ package net.corda.serialization.internal.amqp +import net.corda.core.KeepForDJVM import net.corda.core.serialization.SerializationContext import org.apache.qpid.proton.amqp.Binary import org.apache.qpid.proton.codec.Data @@ -70,6 +71,7 @@ sealed class PropertySerializer(val name: String, val propertyReader: PropertyRe /** * A property serializer for a complex type (another object). */ + @KeepForDJVM class DescribedTypePropertySerializer( name: String, readMethod: PropertyReader, diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializers.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializers.kt index 207f51b9dd..7ae011af08 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializers.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializers.kt @@ -10,6 +10,7 @@ package net.corda.serialization.internal.amqp +import net.corda.core.KeepForDJVM import net.corda.core.utilities.loggerFor import java.io.NotSerializableException import java.lang.reflect.Field @@ -27,6 +28,7 @@ abstract class PropertyReader { /** * Accessor for those properties of a class that have defined getter functions. */ +@KeepForDJVM class PublicPropertyReader(private val readMethod: Method?) : PropertyReader() { init { readMethod?.isAccessible = true @@ -65,6 +67,7 @@ class PublicPropertyReader(private val readMethod: Method?) : PropertyReader() { * Accessor for those properties of a class that do not have defined getter functions. In which case * we used reflection to remove the unreadable status from that property whilst it's accessed. */ +@KeepForDJVM class PrivatePropertyReader(val field: Field, parentType: Type) : PropertyReader() { init { loggerFor().warn("Create property Serializer for private property '${field.name}' not " diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Schema.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Schema.kt index 27950b8a5a..1b37c71653 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Schema.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Schema.kt @@ -10,6 +10,7 @@ package net.corda.serialization.internal.amqp +import net.corda.core.KeepForDJVM import net.corda.core.internal.uncheckedCast import net.corda.serialization.internal.CordaSerializationMagic import org.apache.qpid.proton.amqp.DescribedType @@ -28,6 +29,7 @@ val amqpMagic = CordaSerializationMagic("corda".toByteArray() + byteArrayOf(1, 0 * This and the classes below are OO representations of the AMQP XML schema described in the specification. Their * [toString] representations generate the associated XML form. */ +@KeepForDJVM data class Schema(val types: List) : DescribedType { companion object : DescribedTypeConstructor { val DESCRIPTOR = AMQPDescriptorRegistry.SCHEMA.amqpDescriptor @@ -56,6 +58,7 @@ data class Schema(val types: List) : DescribedType { override fun toString(): String = types.joinToString("\n") } +@KeepForDJVM data class Descriptor(val name: Symbol?, val code: UnsignedLong? = null) : DescribedType { constructor(name: String?) : this(Symbol.valueOf(name)) @@ -96,6 +99,7 @@ data class Descriptor(val name: Symbol?, val code: UnsignedLong? = null) : Descr } } +@KeepForDJVM data class Field( val name: String, val type: String, @@ -163,6 +167,7 @@ sealed class TypeNotation : DescribedType { abstract val descriptor: Descriptor } +@KeepForDJVM data class CompositeType( override val name: String, override val label: String?, @@ -213,6 +218,7 @@ data class CompositeType( } } +@KeepForDJVM data class RestrictedType(override val name: String, override val label: String?, override val provides: List, @@ -263,6 +269,7 @@ data class RestrictedType(override val name: String, } } +@KeepForDJVM data class Choice(val name: String, val value: String) : DescribedType { companion object : DescribedTypeConstructor { val DESCRIPTOR = AMQPDescriptorRegistry.CHOICE.amqpDescriptor @@ -292,6 +299,7 @@ data class Choice(val name: String, val value: String) : DescribedType { } } +@KeepForDJVM data class ReferencedObject(private val refCounter: Int) : DescribedType { companion object : DescribedTypeConstructor { val DESCRIPTOR = AMQPDescriptorRegistry.REFERENCED_OBJECT.amqpDescriptor 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 4b31e83ba6..ed4148147c 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 @@ -12,6 +12,7 @@ package net.corda.serialization.internal.amqp import com.google.common.primitives.Primitives import com.google.common.reflect.TypeToken +import net.corda.core.KeepForDJVM import net.corda.core.internal.isConcreteClass import net.corda.core.internal.isPublic import net.corda.core.serialization.ClassWhitelist @@ -97,6 +98,7 @@ fun propertiesForSerialization( * @property getter the method of a class that returns a fields value. Determined by * locating a function named getXyz for the property named in field as xyz. */ +@KeepForDJVM data class PropertyDescriptor(var field: Field?, var setter: Method?, var getter: Method?, var iser: Method?) { override fun toString() = StringBuilder("").apply { appendln("Property - ${field?.name ?: "null field"}\n") diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationOutput.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationOutput.kt index 1b0252a49e..767fadcce2 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationOutput.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationOutput.kt @@ -10,6 +10,7 @@ package net.corda.serialization.internal.amqp +import net.corda.core.KeepForDJVM import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationEncoding import net.corda.core.serialization.SerializedBytes @@ -23,6 +24,7 @@ import java.lang.reflect.Type import java.util.* import kotlin.collections.LinkedHashSet +@KeepForDJVM data class BytesAndSchemas( val obj: SerializedBytes, val schema: Schema, @@ -34,6 +36,7 @@ data class BytesAndSchemas( * @param serializerFactory This is the factory for [AMQPSerializer] instances and can be shared across multiple * instances and threads. */ +@KeepForDJVM open class SerializationOutput @JvmOverloads constructor( internal val serializerFactory: SerializerFactory, private val encoding: SerializationEncoding? = null diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt index 447c14d915..2ed43e7667 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt @@ -12,6 +12,9 @@ package net.corda.serialization.internal.amqp import com.google.common.primitives.Primitives import com.google.common.reflect.TypeResolver +import net.corda.core.DeleteForDJVM +import net.corda.core.KeepForDJVM +import net.corda.core.StubOutForDJVM import net.corda.core.internal.kotlinObjectInstance import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.ClassWhitelist @@ -27,7 +30,9 @@ import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList import javax.annotation.concurrent.ThreadSafe +@KeepForDJVM data class SerializationSchemas(val schema: Schema, val transforms: TransformsSchema) +@KeepForDJVM data class FactorySchemaAndDescriptor(val schemas: SerializationSchemas, val typeDescriptor: Any) /** @@ -48,6 +53,7 @@ data class FactorySchemaAndDescriptor(val schemas: SerializationSchemas, val typ // TODO: generic types should define restricted type alias with source of the wildcarded version, I think, if we're to generate classes from schema // TODO: need to rethink matching of constructor to properties in relation to implementing interfaces and needing those properties etc. // TODO: need to support super classes as well as interfaces with our current code base... what's involved? If we continue to ban, what is the impact? +@KeepForDJVM @ThreadSafe open class SerializerFactory( val whitelist: ClassWhitelist, @@ -58,6 +64,7 @@ open class SerializerFactory( val serializersByDescriptor: MutableMap>, private val customSerializers: MutableList, val transformsCache: MutableMap>>) { + @DeleteForDJVM constructor(whitelist: ClassWhitelist, classCarpenter: ClassCarpenter, evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), @@ -68,6 +75,7 @@ open class SerializerFactory( customSerializers = CopyOnWriteArrayList(), transformsCache = ConcurrentHashMap()) + @DeleteForDJVM constructor(whitelist: ClassWhitelist, classLoader: ClassLoader, evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), @@ -284,6 +292,7 @@ open class SerializerFactory( } } + @StubOutForDJVM private fun runCarpentry(schemaAndDescriptor: FactorySchemaAndDescriptor, metaSchema: CarpenterMetaSchema) { val mc = MetaCarpenter(metaSchema, classCarpenter) try { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SupportedTransforms.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SupportedTransforms.kt index f1b4ab3ac4..f3efb788de 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SupportedTransforms.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SupportedTransforms.kt @@ -10,6 +10,7 @@ package net.corda.serialization.internal.amqp +import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializationTransformEnumDefault import net.corda.core.serialization.CordaSerializationTransformEnumDefaults import net.corda.core.serialization.CordaSerializationTransformRename @@ -25,6 +26,7 @@ import net.corda.core.serialization.CordaSerializationTransformRenames * that reference the transform. Notionally this allows the code that extracts transforms to work on single instances * of a transform or a meta list of them. */ +@KeepForDJVM data class SupportedTransform( val type: Class, val enum: TransformTypes, diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformTypes.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformTypes.kt index e2719730f8..3b74da01cd 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformTypes.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformTypes.kt @@ -10,6 +10,7 @@ package net.corda.serialization.internal.amqp +import net.corda.core.KeepForDJVM import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.CordaSerializationTransformEnumDefault import net.corda.core.serialization.CordaSerializationTransformEnumDefaults @@ -30,6 +31,7 @@ import java.io.NotSerializableException */ // TODO: it would be awesome to auto build this list by scanning for transform annotations themselves // TODO: annotated with some annotation +@KeepForDJVM enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType { /** * Placeholder entry for future transforms where a node receives a transform we've subsequently diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformsSchema.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformsSchema.kt index c5da38c022..8f055b2244 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformsSchema.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformsSchema.kt @@ -10,6 +10,7 @@ package net.corda.serialization.internal.amqp +import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializationTransformEnumDefault import net.corda.core.serialization.CordaSerializationTransformRename import org.apache.qpid.proton.amqp.DescribedType @@ -313,6 +314,7 @@ data class TransformsSchema(val types: Map = Class.forName(proxy.className, true, factory.classloader) + @KeepForDJVM data class ClassProxy(val className: String) } \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ContractAttachmentSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ContractAttachmentSerializer.kt index 3f0251c270..6a2fb04d70 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ContractAttachmentSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ContractAttachmentSerializer.kt @@ -10,6 +10,7 @@ package net.corda.serialization.internal.amqp.custom +import net.corda.core.KeepForDJVM import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.ContractClassName @@ -39,5 +40,6 @@ class ContractAttachmentSerializer(factory: SerializerFactory) : CustomSerialize return ContractAttachment(proxy.attachment, proxy.contract, proxy.contracts, proxy.uploader) } + @KeepForDJVM data class ContractAttachmentProxy(val attachment: Attachment, val contract: ContractClassName, val contracts: Set, val uploader: String?) } \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/DurationSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/DurationSerializer.kt index 25642ce5c0..919b587a66 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/DurationSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/DurationSerializer.kt @@ -10,6 +10,7 @@ package net.corda.serialization.internal.amqp.custom +import net.corda.core.KeepForDJVM import net.corda.serialization.internal.amqp.CustomSerializer import net.corda.serialization.internal.amqp.SerializerFactory import java.time.Duration @@ -22,5 +23,6 @@ class DurationSerializer(factory: SerializerFactory) : CustomSerializer.Proxy, val elements: List) } \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/InstantSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/InstantSerializer.kt index 650f86e9b1..57fb2030bf 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/InstantSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/InstantSerializer.kt @@ -10,6 +10,7 @@ package net.corda.serialization.internal.amqp.custom +import net.corda.core.KeepForDJVM import net.corda.serialization.internal.amqp.CustomSerializer import net.corda.serialization.internal.amqp.SerializerFactory import java.time.Instant @@ -22,5 +23,6 @@ class InstantSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(SimpleString::class.java) \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt index 5014a2909e..75687bc9ad 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt @@ -12,11 +12,13 @@ package net.corda.serialization.internal.amqp.custom import net.corda.core.CordaRuntimeException import net.corda.core.CordaThrowable +import net.corda.core.KeepForDJVM import net.corda.core.serialization.SerializationFactory import net.corda.core.utilities.contextLogger import net.corda.serialization.internal.amqp.* import java.io.NotSerializableException +@KeepForDJVM class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(Throwable::class.java, ThrowableProxy::class.java, factory) { companion object { @@ -98,5 +100,6 @@ class StackTraceElementSerializer(factory: SerializerFactory) : CustomSerializer override fun fromProxy(proxy: StackTraceElementProxy): StackTraceElement = StackTraceElement(proxy.declaringClass, proxy.methodName, proxy.fileName, proxy.lineNumber) + @KeepForDJVM data class StackTraceElementProxy(val declaringClass: String, val methodName: String, val fileName: String?, val lineNumber: Int) } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/YearMonthSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/YearMonthSerializer.kt index c5bddf00ac..406a058fce 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/YearMonthSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/YearMonthSerializer.kt @@ -10,6 +10,7 @@ package net.corda.serialization.internal.amqp.custom +import net.corda.core.KeepForDJVM import net.corda.serialization.internal.amqp.CustomSerializer import net.corda.serialization.internal.amqp.SerializerFactory import java.time.YearMonth @@ -22,5 +23,6 @@ class YearMonthSerializer(factory: SerializerFactory) : CustomSerializer.Proxy = mutableMapOf() diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt index eeb7404fcb..b6f1b06078 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt @@ -8,9 +8,12 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ +@file:DeleteForDJVM package net.corda.serialization.internal.carpenter import com.google.common.base.MoreObjects +import net.corda.core.DeleteForDJVM +import net.corda.core.KeepForDJVM import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.CordaSerializable import org.objectweb.asm.ClassWriter @@ -32,6 +35,7 @@ interface SimpleFieldAccess { operator fun get(name: String): Any? } +@DeleteForDJVM class CarpenterClassLoader(parentClassLoader: ClassLoader = Thread.currentThread().contextClassLoader) : ClassLoader(parentClassLoader) { fun load(name: String, bytes: ByteArray): Class<*> = defineClass(name, bytes, 0, bytes.size) @@ -56,6 +60,7 @@ private val moreObjects: String = Type.getInternalName(MoreObjects::class.java) private val toStringHelper: String = Type.getInternalName(MoreObjects.ToStringHelper::class.java) // Allow us to create alternative ClassCarpenters. +@KeepForDJVM interface ClassCarpenter { val whitelist: ClassWhitelist val classloader: ClassLoader @@ -106,6 +111,7 @@ interface ClassCarpenter { * * Equals/hashCode methods are not yet supported. */ +@DeleteForDJVM class ClassCarpenterImpl(cl: ClassLoader, override val whitelist: ClassWhitelist) : ClassCarpenter { constructor(whitelist: ClassWhitelist) : this(Thread.currentThread().contextClassLoader, whitelist) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Exceptions.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Exceptions.kt index 30c85e5eff..d0fe14be7a 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Exceptions.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Exceptions.kt @@ -11,6 +11,7 @@ package net.corda.serialization.internal.carpenter import net.corda.core.CordaRuntimeException +import net.corda.core.DeleteForDJVM import org.objectweb.asm.Type /** @@ -26,6 +27,7 @@ abstract class InterfaceMismatchException(msg: String) : ClassCarpenterException class DuplicateNameException(val name: String) : ClassCarpenterException( "An attempt was made to register two classes with the name '$name' within the same ClassCarpenter namespace.") +@DeleteForDJVM class NullablePrimitiveException(val name: String, val field: Class) : ClassCarpenterException( "Field $name is primitive type ${Type.getDescriptor(field)} and thus cannot be nullable") diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/MetaCarpenter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/MetaCarpenter.kt index bb7b2284fb..52b5bb0981 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/MetaCarpenter.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/MetaCarpenter.kt @@ -10,6 +10,9 @@ package net.corda.serialization.internal.carpenter +import net.corda.core.DeleteForDJVM +import net.corda.core.KeepForDJVM +import net.corda.core.StubOutForDJVM import net.corda.serialization.internal.amqp.CompositeType import net.corda.serialization.internal.amqp.RestrictedType import net.corda.serialization.internal.amqp.TypeNotation @@ -33,6 +36,7 @@ import net.corda.serialization.internal.amqp.TypeNotation * in turn look up all of those classes in the [dependsOn] list, remove their dependency on the newly created class, * and if that list is reduced to zero know we can now generate a [Schema] for them and carpent them up */ +@KeepForDJVM data class CarpenterMetaSchema( val carpenterSchemas: MutableList, val dependencies: MutableMap>>, @@ -57,7 +61,8 @@ data class CarpenterMetaSchema( // We could make this an abstract method on TypeNotation but that // would mean the amqp package being "more" infected with carpenter // specific bits. - fun buildFor(target: TypeNotation, cl: ClassLoader) = when (target) { + @StubOutForDJVM + fun buildFor(target: TypeNotation, cl: ClassLoader): Unit = when (target) { is RestrictedType -> target.carpenterSchema(this) is CompositeType -> target.carpenterSchema(cl, this, false) } @@ -72,6 +77,7 @@ data class CarpenterMetaSchema( * @property cc a reference to the actual class carpenter we're using to constuct classes * @property objects a list of carpented classes loaded into the carpenters class loader */ +@DeleteForDJVM abstract class MetaCarpenterBase(val schemas: CarpenterMetaSchema, val cc: ClassCarpenter) { val objects = mutableMapOf>() @@ -101,6 +107,7 @@ abstract class MetaCarpenterBase(val schemas: CarpenterMetaSchema, val cc: Class get() = cc.classloader } +@DeleteForDJVM class MetaCarpenter(schemas: CarpenterMetaSchema, cc: ClassCarpenter) : MetaCarpenterBase(schemas, cc) { override fun build() { while (schemas.carpenterSchemas.isNotEmpty()) { @@ -114,6 +121,7 @@ class MetaCarpenter(schemas: CarpenterMetaSchema, cc: ClassCarpenter) : MetaCarp } } +@DeleteForDJVM class TestMetaCarpenter(schemas: CarpenterMetaSchema, cc: ClassCarpenter) : MetaCarpenterBase(schemas, cc) { override fun build() { if (schemas.carpenterSchemas.isEmpty()) return diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Schema.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Schema.kt index 8e56dfe2c5..fdc719f549 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Schema.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Schema.kt @@ -8,12 +8,16 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ +@file:DeleteForDJVM package net.corda.serialization.internal.carpenter +import net.corda.core.DeleteForDJVM +import net.corda.core.KeepForDJVM import org.objectweb.asm.ClassWriter import org.objectweb.asm.Opcodes.* import java.util.* +@KeepForDJVM enum class SchemaFlags { SimpleFieldAccess, CordaSerializable } @@ -26,6 +30,7 @@ enum class SchemaFlags { * - [InterfaceSchema] * - [EnumSchema] */ +@KeepForDJVM abstract class Schema( val name: String, var fields: Map, @@ -50,6 +55,7 @@ abstract class Schema( fun descriptorsIncludingSuperclasses(): Map = (superclass?.descriptorsIncludingSuperclasses() ?: emptyMap()) + fields.descriptors() + @DeleteForDJVM abstract fun generateFields(cw: ClassWriter) val jvmName: String @@ -74,6 +80,7 @@ fun EnumMap.simpleFieldAccess(): Boolean { /** * Represents a concrete object. */ +@DeleteForDJVM class ClassSchema( name: String, fields: Map, @@ -89,6 +96,7 @@ class ClassSchema( * Represents an interface. Carpented interfaces can be used within [ClassSchema]s * if that class should be implementing that interface. */ +@DeleteForDJVM class InterfaceSchema( name: String, fields: Map, @@ -103,6 +111,7 @@ class InterfaceSchema( /** * Represents an enumerated type. */ +@DeleteForDJVM class EnumSchema( name: String, fields: Map @@ -124,6 +133,7 @@ class EnumSchema( * Factory object used by the serializer when building [Schema]s based * on an AMQP schema. */ +@DeleteForDJVM object CarpenterSchemaFactory { fun newInstance( name: String, diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/SchemaFields.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/SchemaFields.kt index bc9b4d8559..e6f1e08793 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/SchemaFields.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/SchemaFields.kt @@ -11,6 +11,7 @@ package net.corda.serialization.internal.carpenter import jdk.internal.org.objectweb.asm.Opcodes.* +import net.corda.core.DeleteForDJVM import org.objectweb.asm.ClassWriter import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Type @@ -25,7 +26,9 @@ abstract class Field(val field: Class) { var name: String = unsetName abstract val type: String + @DeleteForDJVM abstract fun generateField(cw: ClassWriter) + @DeleteForDJVM abstract fun visitParameter(mv: MethodVisitor, idx: Int) } @@ -36,6 +39,7 @@ abstract class Field(val field: Class) { * - [NullableField] * - [NonNullableField] */ +@DeleteForDJVM abstract class ClassField(field: Class) : Field(field) { abstract val nullabilityAnnotation: String abstract fun nullTest(mv: MethodVisitor, slot: Int) @@ -69,6 +73,7 @@ abstract class ClassField(field: Class) : Field(field) { * * maps to AMQP mandatory = true fields */ +@DeleteForDJVM open class NonNullableField(field: Class) : ClassField(field) { override val nullabilityAnnotation = "Ljavax/annotation/Nonnull;" @@ -99,6 +104,7 @@ open class NonNullableField(field: Class) : ClassField(field) { * * maps to AMQP mandatory = false fields */ +@DeleteForDJVM class NullableField(field: Class) : ClassField(field) { override val nullabilityAnnotation = "Ljavax/annotation/Nullable;" @@ -120,6 +126,7 @@ class NullableField(field: Class) : ClassField(field) { /** * Represents enum constants within an enum */ +@DeleteForDJVM class EnumField : Field(Enum::class.java) { override var descriptor: String? = null @@ -140,6 +147,7 @@ class EnumField : Field(Enum::class.java) { * Constructs a Field Schema object of the correct type depending weather * the AMQP schema indicates it's mandatory (non nullable) or not (nullable) */ +@DeleteForDJVM object FieldFactory { fun newInstance(mandatory: Boolean, name: String, field: Class) = if (mandatory) NonNullableField(name, field) else NullableField(name, field) diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/GenericsTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/GenericsTests.kt index 2c5c5f06cb..40edbd5b83 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/GenericsTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/GenericsTests.kt @@ -22,6 +22,7 @@ import net.corda.serialization.internal.AllWhitelist import net.corda.testing.common.internal.ProjectStructure.projectRootDir import net.corda.testing.core.TestIdentity import org.junit.Test +import java.net.URI import java.util.* import kotlin.test.assertEquals @@ -38,7 +39,7 @@ class GenericsTests { const val VERBOSE = true @Suppress("UNUSED") - var localPath = projectRootDir.toUri().resolve( + var localPath: URI = projectRootDir.toUri().resolve( "serialization/src/test/resources/net/corda/serialization/internal/amqp") val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")) diff --git a/settings.gradle b/settings.gradle index b3387a2a1b..8407af74ce 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,6 +15,10 @@ include 'confidential-identities' include 'finance' include 'finance:isolated' include 'core' +include 'core-deterministic' +include 'core-deterministic:testing' +include 'core-deterministic:testing:common' +include 'core-deterministic:testing:data' include 'docs' include 'node-api' include 'node' @@ -37,6 +41,7 @@ include 'experimental:intellij-plugin' include 'experimental:flow-hook' include 'experimental:ha-testing' include 'experimental:corda-utils' +include 'jdk8u-deterministic' include 'test-common' include 'test-utils' include 'smoke-test-utils' @@ -74,6 +79,7 @@ include 'samples:bank-of-corda-demo' include 'samples:business-network-demo' include 'samples:cordapp-configuration' include 'serialization' +include 'serialization-deterministic' include 'cordform-common' include 'verify-enclave' include 'hsm-tool' diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt index a469e7f314..18dc65568f 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt @@ -137,6 +137,42 @@ class VaultFiller @JvmOverloads constructor( return Vault(states) } + @JvmOverloads + fun fillWithSomeTestLinearAndDealStates(numberToCreate: Int, + externalId: String? = null, + participants: List = emptyList(), + linearString: String = "", + linearNumber: Long = 0L, + linearBoolean: Boolean = false, + linearTimestamp: Instant = now()): Vault { + val myKey: PublicKey = services.myInfo.chooseIdentity().owningKey + val me = AnonymousParty(myKey) + val issuerKey = defaultNotary.keyPair + val signatureMetadata = SignatureMetadata(services.myInfo.platformVersion, Crypto.findSignatureScheme(issuerKey.public).schemeNumberID) + val transactions: List = (1..numberToCreate).map { + val dummyIssue = TransactionBuilder(notary = defaultNotary.party).apply { + // Issue a Linear state + addOutputState(DummyLinearContract.State( + linearId = UniqueIdentifier(externalId), + participants = participants.plus(me), + linearString = linearString, + linearNumber = linearNumber, + linearBoolean = linearBoolean, + linearTimestamp = linearTimestamp), DUMMY_LINEAR_CONTRACT_PROGRAM_ID) + // Issue a Deal state + addOutputState(DummyDealContract.State(ref = "test ref", participants = participants.plus(me)), DUMMY_DEAL_PROGRAM_ID) + addCommand(dummyCommand()) + } + return@map services.signInitialTransaction(dummyIssue).withAdditionalSignature(issuerKey, signatureMetadata) + } + services.recordTransactions(transactions) + // Get all the StateAndRefs of all the generated transactions. + val states = transactions.flatMap { stx -> + stx.tx.outputs.indices.map { i -> stx.tx.outRef(i) } + } + return Vault(states) + } + @JvmOverloads fun fillWithSomeTestCash(howMuch: Amount, issuerServices: ServiceHub, @@ -177,7 +213,6 @@ class VaultFiller @JvmOverloads constructor( return Vault(states) } - /** * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. */