From 6661ee8a3e74e063833a55d833daed2879161c5f Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Wed, 11 Apr 2018 15:32:46 +0100 Subject: [PATCH 1/8] Add doc --- .../Example SGX deployment.png | Bin 0 -> 75957 bytes .../decisions/certification.md | 69 ++++ .../decisions/enclave-language.md | 59 ++++ .../sgx-infrastructure/decisions/kv-store.md | 58 ++++ .../sgx-infrastructure/decisions/roadmap.md | 81 +++++ .../sgx-infrastructure/decisions/roadmap.png | Bin 0 -> 100220 bytes .../design/sgx-infrastructure/design.md | 78 +++++ .../sgx-infrastructure/details/attestation.md | 92 +++++ .../sgx-infrastructure/details/channels.md | 75 +++++ .../sgx-infrastructure/details/discovery.md | 88 +++++ .../details/enclave-deployment.md | 16 + .../details/enclave-storage.md | 7 + .../design/sgx-infrastructure/details/host.md | 11 + .../sgx-infrastructure/details/ias-proxy.md | 10 + .../sgx-infrastructure/details/kv-store.md | 13 + .../sgx-infrastructure/details/serverless.md | 33 ++ .../design/sgx-infrastructure/details/time.md | 69 ++++ .../sgx-integration/SgxProvisioning.png | Bin 0 -> 245235 bytes docs/source/design/sgx-integration/design.md | 315 ++++++++++++++++++ docs/source/index.rst | 1 + 20 files changed, 1075 insertions(+) create mode 100644 docs/source/design/sgx-infrastructure/Example SGX deployment.png create mode 100644 docs/source/design/sgx-infrastructure/decisions/certification.md create mode 100644 docs/source/design/sgx-infrastructure/decisions/enclave-language.md create mode 100644 docs/source/design/sgx-infrastructure/decisions/kv-store.md create mode 100644 docs/source/design/sgx-infrastructure/decisions/roadmap.md create mode 100644 docs/source/design/sgx-infrastructure/decisions/roadmap.png create mode 100644 docs/source/design/sgx-infrastructure/design.md create mode 100644 docs/source/design/sgx-infrastructure/details/attestation.md create mode 100644 docs/source/design/sgx-infrastructure/details/channels.md create mode 100644 docs/source/design/sgx-infrastructure/details/discovery.md create mode 100644 docs/source/design/sgx-infrastructure/details/enclave-deployment.md create mode 100644 docs/source/design/sgx-infrastructure/details/enclave-storage.md create mode 100644 docs/source/design/sgx-infrastructure/details/host.md create mode 100644 docs/source/design/sgx-infrastructure/details/ias-proxy.md create mode 100644 docs/source/design/sgx-infrastructure/details/kv-store.md create mode 100644 docs/source/design/sgx-infrastructure/details/serverless.md create mode 100644 docs/source/design/sgx-infrastructure/details/time.md create mode 100644 docs/source/design/sgx-integration/SgxProvisioning.png create mode 100644 docs/source/design/sgx-integration/design.md 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 0000000000000000000000000000000000000000..1ccdcddc659d980a9ffc2988447e5d9b39f5c9ea GIT binary patch literal 75957 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..038f3430e67c168205f13b132e2413af25495c37 GIT binary patch literal 100220 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⪼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 N7nKny64HD7zW}KKCSm{p literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2c52a3f18042b76a8e6d3f66e3c699eb7280831e GIT binary patch literal 245235 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%0A8yEWXhk>;Q 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

-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ᵈmBFeV(4Ipd;x`1C#XVd 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 literal 0 HcmV?d00001 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/index.rst b/docs/source/index.rst index ff25e2b1a7..cc508c098a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -63,6 +63,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 From 6cc08776b51b8a397a3ed9324bf7eb67dfec9413 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Mon, 11 Jun 2018 17:13:45 +0200 Subject: [PATCH 2/8] Docs: move json to the development section (#3343) --- docs/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index ff25e2b1a7..a7a7d0ed7e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -41,6 +41,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 @@ -72,4 +73,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 From 5ceb61606a7c36bb1f59213c09cffccb57e65f55 Mon Sep 17 00:00:00 2001 From: Dan Newton Date: Mon, 11 Jun 2018 17:53:31 +0100 Subject: [PATCH 3/8] VaultTrack returns undesired states #3276 (#3336) * filter by contract state in _trackBy * write tests to check that _trackBy is filtering the states correct and tidy up filtering functions * remove un needed function * add change log message for filtering unrelated ContractStates from trackBy --- docs/source/changelog.rst | 3 + .../node/services/vault/NodeVaultService.kt | 11 ++- .../node/services/vault/VaultQueryTests.kt | 74 ++++++++++++++++++- .../testing/internal/vault/VaultFiller.kt | 37 +++++++++- 4 files changed, 119 insertions(+), 6 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 22df9ee1b8..f673e74ee8 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -7,6 +7,9 @@ release, see :doc:`upgrade-notes`. Unreleased ========== +* 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 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 06ffc886b2..0826ef094b 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 @@ -489,12 +489,21 @@ class NodeVaultService( return database.transaction { mutex.locked { 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 3bc59abe3f..980f79b29d 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 @@ -28,10 +28,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 @@ -2282,4 +2279,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) {} + } + ) + } + } } \ No newline at end of file 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 6308223bbb..a3cd85d564 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 @@ -127,6 +127,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, @@ -167,7 +203,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. */ From 39dcf472becb9819b8cdfd87844c45ba0f223c50 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Mon, 11 Jun 2018 17:54:27 +0100 Subject: [PATCH 4/8] Update CONTRIBUTORS.md --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index c77a2ba99d..b635b8e832 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -61,6 +61,7 @@ see changes to this list. * cncorda * Credit Suisse * cyrsis +* Dan Newton (Accenture) * Daniel Roig (SEB) * Dave Hudson (R3) * David John Grundy (Dankse Bank) From 4951ad75d55e68b42774537b106039ce9a42e885 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Mon, 11 Jun 2018 18:22:12 +0100 Subject: [PATCH 5/8] Update CONTRIBUTORS.md --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index b635b8e832..45f5e01185 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -68,6 +68,7 @@ see changes to this list. * David Lee (BCS) * Dirk Hermans (KBC) * Edward Greenwood (State Street) +* Elendu Uche (APPZONE) * Farzad Pezeshkpour (RBS) * fracting * Frederic Dalibard (Natixis) From 5d42f48966d3112ecd2cfff0937452ef79f02502 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Mon, 11 Jun 2018 20:34:59 +0100 Subject: [PATCH 6/8] ENT-1463, ENT-1903: Create core-deterministic and serialization-deterministic artifacts. (#3262) * Create core-deterministic and serialization-deterministic artifacts: commit 6f77838fe53d7c9565283c20bbf20f27954b27f6 Author: Chris Rankin Date: Tue May 29 23:14:23 2018 +0100 Tidy up Gradle files. commit 0aa958d31c6342e92ad4d6ab396db6e4a39d4fed Author: Chris Rankin Date: Tue May 29 18:11:17 2018 +0100 Fix EnclaveletTest with deterministic core and serialisation. commit 732fcf37ee2219dfad373200676241d2fd90aeb3 Author: Chris Rankin Date: Sun May 27 00:21:42 2018 +0100 Extend JarFilter to delete typealias declarations. commit 25dbf30ed62c0c059df07782306b7f760f4cdf73 Author: Chris Rankin Date: Thu May 24 17:20:41 2018 +0100 Test deserialising a contract and verifying it using core-deterministic. commit f7753bf2ab588e084cb8bfaa5fd04f1a18d3aaef Author: Chris Rankin Date: Thu May 24 11:25:49 2018 +0100 Do not remove constructors from Kotlin annotation metadata. commit 4ddf357b71b29775aa921aca33b4505a402a20e8 Author: Chris Rankin Date: Wed May 23 16:30:18 2018 +0100 Add Gradle modules for deterministic rt.jar artifacts. commit e81f462eefad2369706fd1b8447d426a71a25a03 Author: Chris Rankin Date: Wed May 23 16:22:04 2018 +0100 Isolate reference to WeakHashMap - for deletion! commit eea2458fbec06b28344547fdf9c191a9445fe1e7 Author: Chris Rankin Date: Thu May 17 18:01:20 2018 +0100 Extract Kotlin metadata classes from kotlin-compiler-embeddable. This fixes a classpath issue that was crashing Gradle. commit 87fdb938d83f3de6589730343c860fbbc406942e Author: Chris Rankin Date: Tue May 15 15:40:31 2018 +0100 Remove instances of ConcurrentHashMap from AMQP serialization scheme. commit 9e7773ec32542af4df62269aea3d08e2bd3794f9 Author: Chris Rankin Date: Mon May 14 23:10:09 2018 +0100 Fix the checkDeterminism targets to validate JAR. commit 6066ba89cb0077b17a7bdda79195763e86d100f9 Author: Chris Rankin Date: Mon May 14 09:16:14 2018 +0100 Remove private Blob object. commit 73180723ad437b07ba4ccfd935620c0fa97039ea Author: Chris Rankin Date: Sun May 13 23:48:48 2018 +0100 Ignore unit tests if important system property is not set. commit abfa0a85aff72007342142a9c66fea3b48f62cc7 Author: Chris Rankin Date: Sat May 12 22:18:28 2018 +0100 Make deterministic tests involving keystores optional. commit 5866f8f08910cbfa90c006e88482acec467042a5 Author: Chris Rankin Date: Fri May 11 17:19:57 2018 +0100 Prevent checked exceptions escaping from JarFilter tasks. commit e2a41913e00aff2bb9b59b43f0a721c5547a8683 Author: Chris Rankin Date: Thu May 10 17:18:30 2018 +0100 Create node-api-deterministic artifact. commit 804feb4e69be4899f29c0cb1c5be95f58d2c47c9 Author: Chris Rankin Date: Thu May 10 16:54:46 2018 +0100 Upgrade to ProGuard 6.0.3 commit ec12b0ed213c1336202012ccf864a49bb8adf727 Author: Chris Rankin Date: Wed May 9 12:56:15 2018 +0100 Extend JarFilter to identify extension properties correctly. commit f0e119e2e3d90db80efb38a316f48b34082c5f49 Author: Chris Rankin Date: Sat May 5 13:47:51 2018 +0100 Construct a better test jar for tasks to unzip/rezip. commit a13380c0ee29dbdd93419f29c01a904c4a69db15 Author: Chris Rankin Date: Fri May 4 09:47:32 2018 +0100 Update JarFilter to Kotlin 1.2.41. commit b774a1e359fb08077a57e8c3b4f1b314653deec0 Author: Chris Rankin Date: Thu May 3 16:27:43 2018 +0100 Convert some JarFilter functions into val properties. commit b38f9a8f53a3e68e62580e0b8af625b37463cd41 Author: Chris Rankin Date: Wed May 2 23:05:24 2018 +0100 Tidy up Gradle test projects. commit 421c2e6c93c0c7317e7977fd7bf134902920760e Author: Chris Rankin Date: Wed May 2 22:12:18 2018 +0100 Published core-deterministic artifact is actually built by metafix task. commit 6d7b20a6826e4c04cd252a4ff4d30ceeb9193eb4 Author: Chris Rankin Date: Wed May 2 16:55:01 2018 +0100 Always recompute compressed sizes for ZipEntry. commit 05587234c4f87aeab925b73f7b7fdc22a2d77159 Author: Chris Rankin Date: Wed May 2 15:14:12 2018 +0100 Test whether MetaFix task can delete file timestamps from the jar. commit 9d6bd0d5cf9f05f088d98eaf7399db4cafc64c61 Author: Chris Rankin Date: Tue May 1 22:15:02 2018 +0100 WIP - erase timestamps for all jar entries. commit 4cb4d6213916d752a654d4fa8d22db6fe6e7e9c6 Author: Chris Rankin Date: Tue May 1 17:39:56 2018 +0100 Annotate more core elements as @Deterministic/@NonDeterministic. commit e847c6c1f03665bd0eff228ce242958512155860 Author: Chris Rankin Date: Tue May 1 17:17:24 2018 +0100 Update JarFilter so that void methods are stubbed with empty bodies. commit f53d7b48676f2b3d2b2062bc12591f9966a8db83 Author: Chris Rankin Date: Tue May 1 16:07:48 2018 +0100 Rename @DeterministicStub to @NonDeterministicStub. commit 0c2e7e76587805b72f0270cdbbc067a909abae82 Author: Chris Rankin Date: Tue May 1 15:57:37 2018 +0100 Consistency fix for JarFilter log messages about methods. commit 43a5c342c508fcc690a02b94926cf4153b5eb297 Author: Chris Rankin Date: Tue May 1 13:25:15 2018 +0100 Reorganise determinism unit test. commit 6079d319d20a6c0cb7386bfcf98b675a73bff040 Author: Chris Rankin Date: Mon Apr 30 23:10:23 2018 +0100 Allow file timestamps to be thrown away for reproducible builds. commit 7068f2fcd46d3f600710ccd9312b9d8dc46f1f38 Author: Chris Rankin Date: Mon Apr 30 21:55:26 2018 +0100 Declare PlatformSecureRandom as non-deterministic. commit 3a5b8eff11a7200f48310408442880967260d80e Author: Chris Rankin Date: Mon Apr 30 17:48:20 2018 +0100 Test deleting property initialisers from block. commit a91f75cf8eb813305adcfd962d8931a1b9322915 Author: Chris Rankin Date: Sun Apr 29 23:34:26 2018 +0100 Suppress lots of "UNUSED" warnings for test classes. commit fb09396f14cb6b2b80e80209091afe370cef15ab Author: Chris Rankin Date: Sun Apr 29 23:10:13 2018 +0100 Add crude test for fixing package metadata. commit 80a9c794bcbc6cbfb7010285c9e94faa9c17310a Author: Chris Rankin Date: Sun Apr 29 21:45:28 2018 +0100 Refactor GradleRunner code into a JUnit Rule. commit 5615dd6624991af49ae283beb3dfe1223d0f26f3 Author: Chris Rankin Date: Sun Apr 29 00:55:10 2018 +0100 Add JaCoCo support to JarFilter plugin. commit df55b962aa77f170d4183865743a263d11f061b3 Author: Chris Rankin Date: Sat Apr 28 16:48:58 2018 +0100 Allow the executor to iterate the Visitor more than twice. commit d906e3996b7724528e69fc4abe79c2b59b2f03cb Author: Chris Rankin Date: Sat Apr 28 16:19:52 2018 +0100 Add tests for deleting static properties. commit f87120efeeb9b6edd129ca71852d1c1bc3fe7e57 Author: Chris Rankin Date: Fri Apr 27 17:34:39 2018 +0100 Ensure that SgxSupport.isInsideEnclave is always true for core-deterministic. commit 85ff9cb17492ae93f0e4f5bbaa2d935e4d776b13 Author: Chris Rankin Date: Fri Apr 27 14:26:14 2018 +0100 Test deleting field references from constructor byte-code. commit ac45aa04c60dab71553ddf0ddfc97ecaed6c84af Author: Chris Rankin Date: Thu Apr 26 22:49:39 2018 +0100 Add tweak to ClassVisitor code. commit 70bc232592e8739546e3f0cdb90add29b5953cf8 Author: Chris Rankin Date: Thu Apr 26 18:37:55 2018 +0100 Declare more Crypto functionality as Deterministic/NonDeterministic. commit 6ceb49af6b75e90ce8e6d739ca6b012627ed6128 Author: Chris Rankin Date: Thu Apr 26 15:39:41 2018 +0100 Rewrite constructors not to reference deleted fields. commit ed1a0e76e68d49531026e130d3c4d4ca56b3e06d Author: Chris Rankin Date: Thu Apr 26 09:48:38 2018 +0100 Configure ASM to compute max stack and locals automatically. commit 0c1a789bf0824b8a18c57a04f4428c678864b76d Author: Chris Rankin Date: Wed Apr 25 17:30:52 2018 +0100 Add isConstructor property to MethodElement. commit acd640db52b2b1051c67067c30414d2035c9d064 Author: Chris Rankin Date: Wed Apr 25 14:59:33 2018 +0100 Add test cases for deleting singleton objects. commit 1a1b79ee13f993dd9cfc9ab8f570e96a5f2e3530 Author: Chris Rankin Date: Wed Apr 25 12:18:49 2018 +0100 Extend JarFilter to delete lazy properties. commit acea7942ad85107e0deec6bef1a0c9d88329b9c4 Author: Chris Rankin Date: Tue Apr 24 16:04:48 2018 +0100 Remove functions for measuring elapsed time. commit 03cc5c53b5b220ceccf43b0a3a218e84055a2f17 Author: Chris Rankin Date: Mon Apr 23 18:29:47 2018 +0100 Modify MetaFixer task to remove deleted nested classes. commit 281c5c06b69fe4bbc28d41aa46c3cf4b6c625877 Author: Chris Rankin Date: Mon Apr 23 15:02:54 2018 +0100 Removing dangling references to deleted nested classes. commit 8bd44ab76dca21b1198db37a1e574538f99c2555 Author: Chris Rankin Date: Sun Apr 22 20:15:00 2018 +0100 Refactor StubbingMethodAdapter to rewrite remaining annotations. commit 59f7392155fe79c9017af563c4705ef5f486dd6b Author: Chris Rankin Date: Sun Apr 22 01:14:59 2018 +0100 Small tidy-up of property tests. commit 7696708ddf3370b13ff5ea2727b2e03380792098 Author: Chris Rankin Date: Sun Apr 22 00:53:28 2018 +0100 Test backing field when deleting a property. commit 083d7678ea73fde03be62d1b845654b9ec9c0c9a Author: Chris Rankin Date: Sun Apr 22 00:40:45 2018 +0100 Refactor isFunction() and isConstructor() for Kotlin reflection. commit cbb5bc30d9fb991d12a8c3775e715b49a2c13abd Author: Chris Rankin Date: Sun Apr 22 00:33:11 2018 +0100 Ensure that stubbed out functions keep their annotations. commit 14947aa105cb7c336b6a7cffa875b6add8000c5d Author: Chris Rankin Date: Sat Apr 21 16:11:33 2018 +0100 Test JarFilter interactions between deleting and stubbing out. commit 2d2a944f56268a697d110923a73589bf71145011 Author: Chris Rankin Date: Fri Apr 20 16:59:40 2018 +0100 Annotate more core classes as @Deterministic. commit a23382ff1930747fa55497fcd8c18e00bf980d4f Author: Chris Rankin Date: Fri Apr 20 15:48:49 2018 +0100 Extend JarFilter unit test coverage. commit af0d3b32c85e23fb7a6c6e9a0639cc0d22a7213f Author: Chris Rankin Date: Fri Apr 20 14:49:56 2018 +0100 Enhance JarFilter - also delete methods that use deleted methods/fields. commit e6cd87e73b5509656faa6879ab8057690c8450ad Author: Chris Rankin Date: Thu Apr 19 18:40:30 2018 +0100 Upgrade AssertJ to 3.9.1 commit ab217563de2cb60a690221d1d497247d04486060 Author: Chris Rankin Date: Thu Apr 19 18:20:54 2018 +0100 Add unit tests for the metadata-fixing task. commit ddff9a10e8aa6dae81b597ff757edee0125d663f Author: Chris Rankin Date: Thu Apr 19 09:01:25 2018 +0100 Refactor Hamcrest matchers into a separate package. commit afa3f5a825f8127bec262ff0a7ece5af1e0c6dfb Author: Chris Rankin Date: Wed Apr 18 23:15:05 2018 +0100 Add unit tests for MethodElement and FieldElement. commit dd412756bc99ff46083558e6863498ae1493a4b7 Author: Chris Rankin Date: Wed Apr 18 17:06:42 2018 +0100 Declare exception and MerkleTree classes as deterministic. commit ce732d2cfb17a8f70a4bc71ccad4d75e68e240c7 Author: Chris Rankin Date: Wed Apr 18 16:04:33 2018 +0100 Never remove @JvmField properties from companion objects when fixing metadata. commit c2a5b35b351480c637dc023c07043243b7f16ee5 Author: Chris Rankin Date: Wed Apr 18 10:37:28 2018 +0100 Rename MetaFix* classes to MetaFixer*. commit 358916bef7eb9955f3fc7cea9ab08286ab153564 Author: Chris Rankin Date: Tue Apr 17 16:21:10 2018 +0100 Extend JarFilter to remove getters/setters along with properties. commit 0c96a154b89244cdc93c53563aacd40b019182d4 Author: Chris Rankin Date: Tue Apr 17 13:11:29 2018 +0100 Extend JarFilter tests for deleting properties. commit bb63fbacbd46e93eb2dbecca21161968d11fc59e Author: Chris Rankin Date: Tue Apr 17 12:28:37 2018 +0100 Fix determination of CordaException. commit cb92d47643e1a9c41267e548fc79d077da941b28 Author: Chris Rankin Date: Tue Apr 17 12:25:06 2018 +0100 Refactor JarFilter - support deleting @JvmField properties. commit 349b1a7fe9bec140e1f988e104ec44a8e65745c6 Author: Chris Rankin Date: Fri Apr 13 17:32:07 2018 +0100 Preliminary Gradle task to remove missing elements from @Metadata. commit f4564e6661458a317f2ebf0e8ce0fbdeae5e1c30 Author: Chris Rankin Date: Tue Apr 3 20:46:41 2018 +0100 Upgrade to ProGuard 6.0.2 commit c937109398c242bb09d0157cec8debded6012a1b Author: Chris Rankin Date: Thu Mar 29 12:04:52 2018 +0100 Refactor internal Kotlin dependencies into MetadataTransformer. commit 899a315a2684986249c88f647784f88235205530 Author: Chris Rankin Date: Thu Mar 29 09:48:05 2018 +0100 Upgrade to ASM 6.1.1. commit 592e1ced7d36f0838c634cb413af9d0b4b8b516b Author: Chris Rankin Date: Sat Mar 24 13:37:17 2018 +0000 Remove unwanted Kotlin artifacts from the JarFilter's classpath. commit 4591d54c247fc9937f202306e2a5ec872fb2dbea Author: Chris Rankin Date: Fri Mar 23 10:04:49 2018 +0000 Tidy up output from Kotlin reflection matchers. commit fb78d898ef1428210bbb030f43b9a2024f1fdeb1 Author: Chris Rankin Date: Fri Mar 23 09:42:38 2018 +0000 Remove lateinit field from ClassTransformer. commit c08ecb2139550ea1bc6ab6cebb3ab180e037c40a Author: Chris Rankin Date: Thu Mar 22 18:25:54 2018 +0000 Remove non-deterministic DelegatingSecureRandom* classes from core-deterministic. commit 7c3e8e794ec868ff4385661ff68081f2bc5ba09c Author: Chris Rankin Date: Thu Mar 22 18:24:48 2018 +0000 Stop removing @kotlin.Metadata annotations from core-deterministic. commit 16ce8ceee91793efb8a100e29d1770f23cf02643 Author: Chris Rankin Date: Thu Mar 22 15:42:58 2018 +0000 Add (C) headers for new files. commit 6146b0b47d9e9f46873506711cbef60477aea655 Author: Chris Rankin Date: Thu Mar 22 12:43:43 2018 +0000 Log synthetic classes, but do not adjust @Metadata. commit 016b2be942533790413e28d50d6dc8b104a4de5c Author: Chris Rankin Date: Thu Mar 22 12:08:36 2018 +0000 Add @Metadata support for Kotlin multi-file classes. commit 9eeed582a083c34a0580f1049cad42d7dc8812a1 Author: Chris Rankin Date: Thu Mar 22 10:38:09 2018 +0000 Add JarFilter unit tests for @kotlin.Metadata updates. commit eb71cb3d76a45fa15eedf478e6172e33a8127305 Author: Chris Rankin Date: Wed Mar 21 15:29:01 2018 +0000 Update JarFilter plugin to remove references to deleted constructors, functions and fields from @kotlin.Metadata. commit c28c099546dd24ab6f158b633e494948fabb6b5e Author: Chris Rankin Date: Thu Mar 15 18:27:06 2018 +0000 Tidy up Enclavelet tests slightly. commit 895dfe659b9ffa6e39b407606876facc153e3128 Author: Chris Rankin Date: Thu Mar 15 18:25:14 2018 +0000 Annotate more Attachment / Transaction classes as @Deterministic. commit f5ab283d09a803b9e2e0f465841cd072e9a7040f Author: Chris Rankin Date: Thu Mar 15 14:21:51 2018 +0000 Upgrade to ProGuard 6.0.1 commit c7717cc0106f39fec822bce8fbbcf18a75a25c2d Author: Chris Rankin Date: Thu Mar 15 11:11:34 2018 +0000 Adjust LedgerTransaction to remove ClassLoader references when deterministic. commit 5b37fe9f3f716944f2eb3952870d2e9548dc144d Author: Chris Rankin Date: Wed Mar 14 16:28:41 2018 +0000 Extra testing for deterministic SecureHash. commit 01be61676edddf28d4b16a75cff1dd5fe2079c03 Author: Chris Rankin Date: Mon Mar 12 16:32:05 2018 +0000 Keep synthetic methods for Kotlin classes. commit cb01f28089c94457c0498802741dcc742a52eaac Author: Chris Rankin Date: Mon Mar 12 14:35:17 2018 +0000 Add libraries to core-deterministic's runtimeArtifacts configuration. commit c23ad307596c07a608d6ce3e600fe1b0aee94ef1 Author: Chris Rankin Date: Mon Mar 12 11:20:35 2018 +0000 Check that JarFilter's different annotations are all distinct. commit 4b84451f9d124cba75bb4a1984b9a9d9f60efd17 Author: Chris Rankin Date: Fri Mar 9 17:01:15 2018 +0000 Update the JarFilter plugin to remove some annotations. This is for stripping @kotlin.Metadata from deterministic classes. commit 72c4740ffdd5fcb9a7828a1324f6632747fe3115 Author: Chris Rankin Date: Fri Mar 9 14:11:16 2018 +0000 Configure ProGuard to preverify the deterministic JAR. commit 9fce4724ac3e1cb80f89d38f63a28b39585dfbf9 Author: Chris Rankin Date: Fri Mar 9 14:09:33 2018 +0000 Update to corda-gradle-plugins 4.0.7-SNAPSHOT. commit fc46624ea2f1c862c9b2a2064a9007ffdc1b94d8 Author: Chris Rankin Date: Thu Mar 8 18:08:20 2018 +0000 Allow core-deterministic artifact to be tested and published. commit 238814ad2d94dd74fd7cbae7dc3b4d1016697850 Author: Chris Rankin Date: Thu Mar 8 14:54:27 2018 +0000 Since Kotlin 1.2.x, Kotlin artifact dependencies match Kotlin plugin version by default. commit f81b3772b598995d0df0519512ae1c6b1d4d238b Author: Chris Rankin Date: Wed Mar 7 13:46:41 2018 +0000 Update KDoc for @Deterministic annotation. commit 7a1b0fbe6540958bbc743981a3ba724f0f22ef80 Author: Chris Rankin Date: Wed Mar 7 12:27:22 2018 +0000 Add (C) headers for JarFilter and deterministic core. commit 0add901e55a23c898da7c6a3ec0c4273d7555441 Author: Chris Rankin Date: Wed Mar 7 09:30:38 2018 +0000 Refactor function name for compatibility with corda-gradle-plugins. commit f37a73dea8969a82ceda48072cb7d393c05a44c7 Author: Chris Rankin Date: Tue Mar 6 13:57:58 2018 +0000 Include more contract states in core-deterministic. commit b2eeb08be90fa1a0739854d0c393a23b8c49aed0 Author: Chris Rankin Date: Tue Mar 6 11:18:53 2018 +0000 Remove synchronized section from deterministic ToggleField. commit 353257e6a04de1447c674f43989e2fc8aecc807a Author: Chris Rankin Date: Fri Mar 2 15:24:46 2018 +0000 Extend @NonDeterministic also to target JVM fields. commit 9dc940c4f9ae8e29e043cdf93634d072373eb030 Author: Chris Rankin Date: Fri Mar 2 15:21:03 2018 +0000 Add tests for deleting field. commit 2bf43957ed656c419cbf1a0a0ba48b755b8e8ac9 Author: Chris Rankin Date: Fri Mar 2 14:16:33 2018 +0000 Tidy up Kotlin lambdas. commit 45dc150cfc0b7090816036a4f4f3ce7ae5cde79b Author: Chris Rankin Date: Fri Mar 2 10:27:57 2018 +0000 Set 'gradle.user.home' for test-kit's GradleRunner. This allows it to share the project's module cache, which means that it doesn't need to download its own copy of Kotlin over the Internet. commit d79ffd0b44cc890dc8e0f513e5d5baaeaddb5d50 Author: Chris Rankin Date: Fri Mar 2 00:41:00 2018 +0000 Remove settings.gradle from tests - it was not the solution. commit b30fdcd4c2b44370294ae78699b1424e817b13de Author: Chris Rankin Date: Fri Mar 2 00:28:42 2018 +0000 Create plugin descriptor using java-gradle-plugin. commit a9e7cbe51e5d3f0d8efea0501ef4858fd3511cd0 Author: Chris Rankin Date: Wed Feb 28 16:55:53 2018 +0000 Resolve simple compiler warning. commit d247524090539a0d708d383f25e9539a6e6ee809 Author: Chris Rankin Date: Wed Feb 28 16:03:19 2018 +0000 Add local settings.gradle for all unit tests. commit 031411c71fda98511f9fba6c763cb6d3f74d95eb Author: Chris Rankin Date: Wed Feb 28 13:58:06 2018 +0000 Add test filtering interface functions. commit dcc6055ae01fb9e98bea73befe7a5cf473e27590 Author: Chris Rankin Date: Wed Feb 28 13:45:25 2018 +0000 Add test for filtering abstract functions. commit 0c084f96aa4cbf7173f633dd1d4fa6e633cea6a7 Author: Chris Rankin Date: Wed Feb 28 11:26:27 2018 +0000 Add tests for stubbing static functions out. commit 3412e3479f09f36e34a33bbd7564bd95b4bbd017 Author: Chris Rankin Date: Wed Feb 28 11:13:35 2018 +0000 Add tests for deleting static functions. commit 5d8ce9ce1edbee0020595af99c20268de8c38c5f Author: Chris Rankin Date: Wed Feb 28 10:50:03 2018 +0000 Add test for stubbing out a var property. commit dea60c8252b0bc849845fdeecc28f67817ef77d8 Author: Chris Rankin Date: Wed Feb 28 10:41:13 2018 +0000 Add test for stubbing a val property out. commit c69de1b904b496fe146e91eb7e6d138171528b1a Author: Chris Rankin Date: Wed Feb 28 10:28:13 2018 +0000 Add tests for stubbing constructors out. commit 1f791cf6013700689e38b129460eba1d20dc5efa Author: Chris Rankin Date: Wed Feb 28 00:35:23 2018 +0000 Add tests for deleting constructors. commit 55790a8abb3dba50b4a136760c9a21dc1bd214ca Author: Chris Rankin Date: Tue Feb 27 18:37:51 2018 +0000 Add (and fix) test for stubbing a function out. commit 1f03202197a9e1fe9023848869e0273a05eef3dc Author: Chris Rankin Date: Tue Feb 27 13:09:55 2018 +0000 Refactor buildSrc into a multi-module project. commit 4c937580f40753408b6f29cfc72741b412e4ed3e Author: Chris Rankin Date: Mon Feb 26 17:15:50 2018 +0000 Initial unit testing framework for JarFilter plugin. commit 45afcaa082cb3f7223d42458a28af14c7c02d611 Author: Chris Rankin Date: Fri Feb 23 12:32:04 2018 +0000 Allow some methods to be stubbed out instead of deleted. commit c5911ec643739369e138a5b451cafa7c067c4134 Author: Chris Rankin Date: Thu Feb 22 10:31:18 2018 +0000 ENT-1468: Initial version of the JarFilterTask. * Refactor deterministic test data to work on Windows. * Fix JarFilter unit tests on Windows. * Upgrade JarFilter to ASM 6.2 * Allow core-deterministic, serialization-deterministic to be published to Artifactory. * Share repository configuration between all JarFilter unit tests. * Small fixes after review. * Ensure core-deterministic and serialization-deterministic are published with their dependencies. * Fix logic for number of JarFilter passes. * Add README for JarFilter plugin. * Move JarFilter plugin into the 'net.corda.plugins' namespace. * Modify JarFilter to update sealed subclasses in @kotlin.Metadata. * Add Gradle fixes from @Clintonio. * Annotate TransientClassWhitelist as deterministic. * Add literalinclude blocks to the deterministic-module docs. * Fix Kotlin Metadata properly when all nested classes are deleted. * Small tidy-up for Gradle files. * Add some KDoc for the JarFilter and deterministic classes. * Update JarFilter to handle properties with generic return types. * Remove some uses of Java Reflection from DJVM. * Rename determinism annotations to @KeepForDJVM, @DeleteForDJVM, @StubOutForDJVM. --- build.gradle | 32 +- buildSrc/build.gradle | 40 +- buildSrc/jarfilter/README.md | 152 ++++++++ buildSrc/jarfilter/build.gradle | 46 +++ .../jarfilter/kotlin-metadata/build.gradle | 81 ++++ .../gradle/jarfilter/ClassTransformer.kt | 353 ++++++++++++++++++ .../net/corda/gradle/jarfilter/Elements.kt | 110 ++++++ .../corda/gradle/jarfilter/JarFilterPlugin.kt | 14 + .../corda/gradle/jarfilter/JarFilterTask.kt | 254 +++++++++++++ .../gradle/jarfilter/KotlinAwareVisitor.kt | 109 ++++++ .../corda/gradle/jarfilter/MetaFixerTask.kt | 128 +++++++ .../gradle/jarfilter/MetaFixerTransformer.kt | 258 +++++++++++++ .../gradle/jarfilter/MetaFixerVisitor.kt | 76 ++++ .../gradle/jarfilter/MetadataTransformer.kt | 307 +++++++++++++++ .../net/corda/gradle/jarfilter/Repeatable.kt | 8 + .../net/corda/gradle/jarfilter/Utils.kt | 89 +++++ .../gradle/jarfilter/AbstractFunctionTest.kt | 69 ++++ .../gradle/jarfilter/DeleteAndStubTests.kt | 188 ++++++++++ .../gradle/jarfilter/DeleteConstructorTest.kt | 165 ++++++++ .../DeleteExtensionValPropertyTest.kt | 52 +++ .../corda/gradle/jarfilter/DeleteFieldTest.kt | 99 +++++ .../gradle/jarfilter/DeleteFunctionTest.kt | 81 ++++ .../corda/gradle/jarfilter/DeleteLazyTest.kt | 71 ++++ .../gradle/jarfilter/DeleteMultiFileTest.kt | 90 +++++ .../gradle/jarfilter/DeleteNestedClassTest.kt | 90 +++++ .../gradle/jarfilter/DeleteObjectTest.kt | 89 +++++ .../jarfilter/DeleteSealedSubclassTest.kt | 56 +++ .../gradle/jarfilter/DeleteStaticFieldTest.kt | 74 ++++ .../jarfilter/DeleteStaticFunctionTest.kt | 87 +++++ .../jarfilter/DeleteStaticValPropertyTest.kt | 91 +++++ .../jarfilter/DeleteStaticVarPropertyTest.kt | 106 ++++++ .../jarfilter/DeleteTypeAliasFromFileTest.kt | 48 +++ .../gradle/jarfilter/DeleteValPropertyTest.kt | 102 +++++ .../gradle/jarfilter/DeleteVarPropertyTest.kt | 141 +++++++ .../net/corda/gradle/jarfilter/DummyJar.kt | 104 ++++++ .../corda/gradle/jarfilter/EmptyPackage.kt | 8 + .../gradle/jarfilter/FieldElementTest.kt | 32 ++ .../gradle/jarfilter/FieldRemovalTest.kt | 212 +++++++++++ .../gradle/jarfilter/InterfaceFunctionTest.kt | 61 +++ .../jarfilter/JarFilterConfigurationTest.kt | 272 ++++++++++++++ .../gradle/jarfilter/JarFilterProject.kt | 56 +++ .../jarfilter/JarFilterTimestampTest.kt | 107 ++++++ .../gradle/jarfilter/MetaFixAnnotationTest.kt | 47 +++ .../jarfilter/MetaFixConfigurationTests.kt | 79 ++++ .../jarfilter/MetaFixConstructorTest.kt | 55 +++ .../gradle/jarfilter/MetaFixFunctionTest.kt | 53 +++ .../jarfilter/MetaFixNestedClassTest.kt | 57 +++ .../gradle/jarfilter/MetaFixPackageTest.kt | 66 ++++ .../corda/gradle/jarfilter/MetaFixProject.kt | 57 +++ .../jarfilter/MetaFixSealedClassTest.kt | 37 ++ .../gradle/jarfilter/MetaFixTimestampTest.kt | 108 ++++++ .../jarfilter/MetaFixValPropertyTest.kt | 49 +++ .../jarfilter/MetaFixVarPropertyTest.kt | 49 +++ .../gradle/jarfilter/MethodElementTest.kt | 88 +++++ .../gradle/jarfilter/RemoveAnnotationsTest.kt | 176 +++++++++ .../jarfilter/StaticFieldRemovalTest.kt | 102 +++++ .../corda/gradle/jarfilter/StdOutLogging.kt | 262 +++++++++++++ .../gradle/jarfilter/StubConstructorTest.kt | 160 ++++++++ .../gradle/jarfilter/StubFunctionOutTest.kt | 74 ++++ .../jarfilter/StubStaticFunctionTest.kt | 127 +++++++ .../gradle/jarfilter/StubValPropertyTest.kt | 46 +++ .../gradle/jarfilter/StubVarPropertyTest.kt | 70 ++++ .../net/corda/gradle/jarfilter/Utilities.kt | 82 ++++ .../net/corda/gradle/jarfilter/UtilsTest.kt | 42 +++ .../gradle/jarfilter/annotations/Deletable.kt | 8 + .../corda/gradle/jarfilter/asm/AsmTools.kt | 47 +++ .../gradle/jarfilter/asm/ClassMetadata.kt | 47 +++ .../gradle/jarfilter/asm/FileMetadata.kt | 33 ++ .../gradle/jarfilter/asm/MetadataTools.kt | 86 +++++ .../gradle/jarfilter/matcher/JavaMatchers.kt | 79 ++++ .../jarfilter/matcher/KotlinMatchers.kt | 193 ++++++++++ .../resources/abstract-function/build.gradle | 34 ++ .../net/corda/gradle/AbstractFunctions.kt | 13 + .../net/corda/gradle/jarfilter/DeleteMe.kt | 20 + .../net/corda/gradle/jarfilter/RemoveMe.kt | 19 + .../net/corda/gradle/jarfilter/StubMeOut.kt | 15 + .../resources/delete-and-stub/build.gradle | 34 ++ .../corda/gradle/DeletePackageWithStubbed.kt | 12 + .../corda/gradle/HasDeletedInsideStubbed.kt | 28 ++ .../gradle/HasPropertyForDeleteAndStub.kt | 21 ++ .../resources/delete-constructor/build.gradle | 33 ++ .../corda/gradle/HasConstructorToDelete.kt | 15 + .../gradle/PrimaryConstructorsToDelete.kt | 21 ++ .../delete-extension-val/build.gradle | 33 ++ .../net/corda/gradle/HasValExtension.kt | 10 + .../test/resources/delete-field/build.gradle | 32 ++ .../net/corda/gradle/HasFieldToDelete.kt | 23 ++ .../delete-file-typealias/build.gradle | 32 ++ .../net/corda/gradle/FileWithTypeAlias.kt | 12 + .../resources/delete-function/build.gradle | 33 ++ .../net/corda/gradle/HasFunctionToDelete.kt | 12 + .../gradle/HasIndirectFunctionToDelete.kt | 13 + .../test/resources/delete-lazy/build.gradle | 33 ++ .../kotlin/net/corda/gradle/HasLazy.kt | 13 + .../resources/delete-multifile/build.gradle | 32 ++ .../kotlin/net/corda/gradle/HasInt.kt | 9 + .../kotlin/net/corda/gradle/HasLong.kt | 9 + .../kotlin/net/corda/gradle/HasString.kt | 9 + .../delete-nested-class/build.gradle | 32 ++ .../net/corda/gradle/HasNestedClasses.kt | 10 + .../kotlin/net/corda/gradle/SealedClass.kt | 10 + .../test/resources/delete-object/build.gradle | 33 ++ .../kotlin/net/corda/gradle/HasObjects.kt | 20 + .../delete-sealed-subclass/build.gradle | 33 ++ .../net/corda/gradle/SealedWithSubclasses.kt | 12 + .../delete-static-field/build.gradle | 32 ++ .../net/corda/gradle/StaticFieldsToDelete.kt | 17 + .../delete-static-function/build.gradle | 33 ++ .../corda/gradle/StaticFunctionsToDelete.kt | 14 + .../resources/delete-static-val/build.gradle | 32 ++ .../net/corda/gradle/StaticValToDelete.kt | 17 + .../resources/delete-static-var/build.gradle | 32 ++ .../net/corda/gradle/StaticVarToDelete.kt | 19 + .../delete-val-property/build.gradle | 33 ++ .../corda/gradle/HasValPropertyForDelete.kt | 11 + .../delete-var-property/build.gradle | 33 ++ .../corda/gradle/HasVarPropertyForDelete.kt | 14 + .../src/test/resources/gradle.properties | 1 + .../resources/interface-function/build.gradle | 34 ++ .../net/corda/gradle/InterfaceFunctions.kt | 13 + .../resources/remove-annotations/build.gradle | 33 ++ .../corda/gradle/HasUnwantedAnnotations.kt | 33 ++ .../src/test/resources/repositories.gradle | 4 + .../src/test/resources/settings.gradle | 6 + .../resources/stub-constructor/build.gradle | 33 ++ .../net/corda/gradle/HasConstructorToStub.kt | 15 + .../corda/gradle/PrimaryConstructorsToStub.kt | 21 ++ .../test/resources/stub-function/build.gradle | 33 ++ .../net/corda/gradle/HasFunctionToStub.kt | 14 + .../net/corda/gradle/RuntimeAnnotations.kt | 11 + .../stub-static-function/build.gradle | 33 ++ .../net/corda/gradle/StaticFunctionsToStub.kt | 22 ++ .../resources/stub-val-property/build.gradle | 33 ++ .../net/corda/gradle/HasValPropertyForStub.kt | 7 + .../resources/stub-var-property/build.gradle | 33 ++ .../net/corda/gradle/HasVarPropertyForStub.kt | 10 + buildSrc/jarfilter/unwanteds/build.gradle | 12 + .../net/corda/gradle/unwanted/HasData.kt | 16 + .../corda/gradle/unwanted/HasUnwantedFun.kt | 5 + .../corda/gradle/unwanted/HasUnwantedVal.kt | 5 + .../corda/gradle/unwanted/HasUnwantedVar.kt | 5 + .../net/corda/gradle/unwanted/HasVal.kt | 17 + .../net/corda/gradle/unwanted/HasVar.kt | 17 + buildSrc/settings.gradle | 3 + .../amqp/AMQPClientSerializationScheme.kt | 6 +- constants.properties | 3 +- core-deterministic/build.gradle | 199 ++++++++++ .../net/corda/core/internal/ToggleField.kt | 62 +++ .../serialization/SerializationFactory.kt | 95 +++++ core-deterministic/testing/build.gradle | 16 + .../testing/common/build.gradle | 24 ++ .../common/LocalSerializationRule.kt | 85 +++++ .../common/MockContractAttachment.kt | 15 + .../corda/deterministic/common/SampleData.kt | 6 + .../common/TransactionVerificationRequest.kt | 32 ++ core-deterministic/testing/data/build.gradle | 29 ++ .../corda/deterministic/data/GenerateData.kt | 92 +++++ .../deterministic/data/KeyStoreGenerator.kt | 51 +++ .../data/TransactionGenerator.kt | 112 ++++++ .../deterministic/CheatingSecurityProvider.kt | 43 +++ .../corda/deterministic/CordaExceptionTest.kt | 70 ++++ .../corda/deterministic/KeyStoreProvider.kt | 44 +++ .../net/corda/deterministic/Utilities.kt | 14 + .../deterministic/contracts/AttachmentTest.kt | 77 ++++ .../contracts/PrivacySaltTest.kt | 34 ++ .../deterministic/crypto/MerkleTreeTest.kt | 14 + .../deterministic/crypto/SecureHashTest.kt | 42 +++ .../deterministic/crypto/SecureRandomTest.kt | 22 ++ .../crypto/TransactionSignatureTest.kt | 143 +++++++ .../TransactionWithSignaturesTest.kt | 30 ++ .../deterministic/txverify/EnclaveletTest.kt | 52 +++ .../src/test/resources/log4j2-test.xml | 14 + core/build.gradle | 15 +- .../java/net/corda/core/crypto/Base58.java | 2 + .../core/flows/IdentifiableException.java | 3 + .../net/corda/core/ClientRelevantError.kt | 1 + .../kotlin/net/corda/core/CordaException.kt | 3 + .../kotlin/net/corda/core/CordaInternal.kt | 11 +- .../main/kotlin/net/corda/core/CordaOID.kt | 1 + .../kotlin/net/corda/core/DeleteForDJVM.kt | 26 ++ .../main/kotlin/net/corda/core/KeepForDJVM.kt | 19 + .../kotlin/net/corda/core/StubOutForDJVM.kt | 24 ++ .../corda/core/concurrent/ConcurrencyUtils.kt | 1 - .../corda/core/context/InvocationContext.kt | 12 + .../kotlin/net/corda/core/context/Trace.kt | 4 + .../kotlin/net/corda/core/contracts/Amount.kt | 2 + .../net/corda/core/contracts/Attachment.kt | 2 + .../core/contracts/AttachmentConstraint.kt | 5 + .../core/contracts/ContractAttachment.kt | 2 + .../net/corda/core/contracts/ContractState.kt | 2 + .../net/corda/core/contracts/ContractsDSL.kt | 3 +- .../net/corda/core/contracts/FungibleAsset.kt | 2 + .../net/corda/core/contracts/Structures.kt | 24 +- .../net/corda/core/contracts/TimeWindow.kt | 4 + .../corda/core/contracts/TransactionState.kt | 2 + .../TransactionVerificationException.kt | 16 + .../corda/core/contracts/UniqueIdentifier.kt | 8 +- .../kotlin/net/corda/core/cordapp/Cordapp.kt | 2 + .../net/corda/core/cordapp/CordappContext.kt | 2 + .../net/corda/core/cordapp/CordappProvider.kt | 2 + .../net/corda/core/crypto/CompositeKey.kt | 5 + .../corda/core/crypto/CompositeKeyFactory.kt | 2 + .../corda/core/crypto/CompositeSignature.kt | 2 + .../crypto/CompositeSignaturesWithKeys.kt | 2 + .../core/crypto/CordaSecurityProvider.kt | 18 +- .../kotlin/net/corda/core/crypto/Crypto.kt | 7 + .../net/corda/core/crypto/CryptoUtils.kt | 8 + .../net/corda/core/crypto/DigitalSignature.kt | 3 + .../net/corda/core/crypto/MerkleTree.kt | 5 +- .../kotlin/net/corda/core/crypto/NullKeys.kt | 2 + .../corda/core/crypto/PartialMerkleTree.kt | 9 +- .../net/corda/core/crypto/SecureHash.kt | 5 + .../net/corda/core/crypto/SignableData.kt | 2 + .../corda/core/crypto/SignatureMetadata.kt | 2 + .../net/corda/core/crypto/SignatureScheme.kt | 2 + .../net/corda/core/crypto/SignedData.kt | 2 + .../corda/core/crypto/TransactionSignature.kt | 2 + .../crypto/internal/PlatformSecureRandom.kt | 18 + .../corda/core/crypto/internal/ProviderMap.kt | 12 +- .../net/corda/core/flows/StateMachineRunId.kt | 2 + .../net/corda/core/identity/AnonymousParty.kt | 2 + .../net/corda/core/identity/CordaX500Name.kt | 2 + .../kotlin/net/corda/core/identity/Party.kt | 2 + .../core/identity/PartyAndCertificate.kt | 2 + .../corda/core/internal/AbstractAttachment.kt | 5 + .../core/internal/ContractUpgradeUtils.kt | 2 + .../kotlin/net/corda/core/internal/Emoji.kt | 3 + .../net/corda/core/internal/FlowIORequest.kt | 2 + .../corda/core/internal/FlowStateMachine.kt | 5 +- .../net/corda/core/internal/InternalUtils.kt | 33 +- .../net/corda/core/internal/LazyPool.kt | 2 + .../net/corda/core/internal/LazyStickyPool.kt | 2 + .../corda/core/internal/LegalNameValidator.kt | 9 + .../net/corda/core/internal/LifeCycle.kt | 2 + .../net/corda/core/internal/PathUtils.kt | 2 + .../core/internal/ResolveTransactionsFlow.kt | 3 + .../net/corda/core/internal/ThreadBox.kt | 2 + .../corda/core/internal/X509EdDSAEngine.kt | 2 + .../core/internal/cordapp/CordappImpl.kt | 2 + .../core/internal/notary/NotaryService.kt | 2 + .../net/corda/core/node/AppServiceHub.kt | 2 + .../net/corda/core/node/NetworkParameters.kt | 3 + .../kotlin/net/corda/core/node/ServiceHub.kt | 3 + .../core/node/services/NetworkMapCache.kt | 3 +- .../core/node/services/TimeWindowChecker.kt | 2 + .../core/node/services/TransactionStorage.kt | 2 + .../services/TransactionVerifierService.kt | 2 + .../corda/core/node/services/VaultService.kt | 2 + .../net/corda/core/schemas/PersistentTypes.kt | 7 +- .../MissingAttachmentsException.kt | 2 + .../core/serialization/SerializationAPI.kt | 20 +- .../SerializationCustomSerializer.kt | 3 + .../core/serialization/SerializationToken.kt | 6 + .../serialization/SerializationWhitelist.kt | 3 + .../internal/SerializationEnvironment.kt | 4 + .../core/transactions/BaseTransaction.kt | 2 + .../ContractUpgradeTransactions.kt | 4 + .../core/transactions/LedgerTransaction.kt | 9 +- .../core/transactions/MerkleTransaction.kt | 5 + .../MissingContractAttachments.kt | 2 + .../transactions/NotaryChangeTransactions.kt | 6 + .../core/transactions/SignedTransaction.kt | 14 + .../core/transactions/TransactionBuilder.kt | 2 + .../transactions/TransactionWithSignatures.kt | 2 + .../core/transactions/WireTransaction.kt | 10 +- .../net/corda/core/utilities/ByteArrays.kt | 5 + .../net/corda/core/utilities/EncodingUtils.kt | 2 + .../kotlin/net/corda/core/utilities/Id.kt | 2 + .../net/corda/core/utilities/KotlinUtils.kt | 4 + .../net/corda/core/utilities/NonEmptySet.kt | 2 + .../kotlin/net/corda/core/utilities/Try.kt | 3 + .../corda/core/utilities/UntrustworthyData.kt | 7 +- .../net/corda/core/utilities/UuidGenerator.kt | 2 + create-jdk8u/.gitignore | 2 + create-jdk8u/Makefile | 25 ++ create-jdk8u/build.gradle | 159 ++++++++ create-jdk8u/settings.gradle | 1 + docs/source/deterministic-modules.rst | 102 ++--- jdk8u-deterministic/build.gradle | 30 ++ .../events/ScheduledFlowIntegrationTests.kt | 10 - .../net/corda/testMessage/ScheduledState.kt | 10 - .../net/corda/node/serialization/kryo/Kryo.kt | 2 + serialization-deterministic/build.gradle | 186 +++++++++ .../internal/AttachmentsClassLoaderBuilder.kt | 13 + .../internal/ByteBufferStreams.kt | 15 + .../internal/DefaultWhitelist.kt | 10 + .../internal/amqp/AMQPSerializerFactories.kt | 34 ++ .../internal/amqp/AMQPStreams.kt | 40 ++ .../internal/AllButBlacklisted.kt | 2 + .../internal/AttachmentsClassLoader.kt | 2 + .../internal/ByteBufferStreams.kt | 6 +- .../serialization/internal/ClassWhitelists.kt | 6 + .../serialization/internal/ClientContexts.kt | 3 +- .../internal/GeneratedAttachment.kt | 2 + .../corda/serialization/internal/OrdinalIO.kt | 3 + .../internal/SerializationFormat.kt | 4 + .../internal/SerializationScheme.kt | 9 +- .../internal/SerializeAsTokenContextImpl.kt | 3 + .../serialization/internal/ServerContexts.kt | 3 +- .../serialization/internal/SharedContexts.kt | 5 + .../internal/UseCaseAwareness.kt | 2 + .../internal/amqp/AMQPSerializationScheme.kt | 7 + .../internal/amqp/AMQPSerializer.kt | 2 + .../internal/amqp/AMQPStreams.kt | 3 +- .../internal/amqp/ArraySerializer.kt | 2 + .../internal/amqp/CollectionSerializer.kt | 2 + .../internal/amqp/DeserializationInput.kt | 2 + .../amqp/DeserializedParameterizedType.kt | 2 + .../serialization/internal/amqp/Envelope.kt | 2 + .../internal/amqp/EvolutionSerializer.kt | 3 + .../internal/amqp/FingerPrinter.kt | 3 + .../internal/amqp/MapSerializer.kt | 4 + .../internal/amqp/PropertySerializer.kt | 2 + .../internal/amqp/PropertySerializers.kt | 3 + .../serialization/internal/amqp/Schema.kt | 8 + .../internal/amqp/SerializationHelper.kt | 2 + .../internal/amqp/SerializationOutput.kt | 3 + .../internal/amqp/SerializerFactory.kt | 9 + .../internal/amqp/SupportedTransforms.kt | 2 + .../internal/amqp/TransformTypes.kt | 2 + .../internal/amqp/TransformsSchema.kt | 2 + .../internal/amqp/custom/BitSetSerializer.kt | 2 + .../amqp/custom/CertPathSerializer.kt | 2 + .../internal/amqp/custom/ClassSerializer.kt | 2 + .../custom/ContractAttachmentSerializer.kt | 2 + .../amqp/custom/DurationSerializer.kt | 2 + .../internal/amqp/custom/EnumSetSerializer.kt | 2 + .../internal/amqp/custom/InstantSerializer.kt | 2 + .../amqp/custom/LocalDateSerializer.kt | 2 + .../amqp/custom/LocalDateTimeSerializer.kt | 2 + .../amqp/custom/LocalTimeSerializer.kt | 2 + .../amqp/custom/MonthDaySerializer.kt | 2 + .../amqp/custom/OffsetDateTimeSerializer.kt | 2 + .../amqp/custom/OffsetTimeSerializer.kt | 2 + .../internal/amqp/custom/PeriodSerializer.kt | 2 + .../amqp/custom/SimpleStringSerializer.kt | 2 + .../amqp/custom/ThrowableSerializer.kt | 3 + .../amqp/custom/YearMonthSerializer.kt | 2 + .../internal/amqp/custom/YearSerializer.kt | 2 + .../internal/amqp/custom/ZoneIdSerializer.kt | 2 + .../amqp/custom/ZonedDateTimeSerializer.kt | 2 + .../carpenter/AMQPSchemaExtensions.kt | 4 + .../internal/carpenter/ClassCarpenter.kt | 6 + .../internal/carpenter/Exceptions.kt | 2 + .../internal/carpenter/MetaCarpenter.kt | 10 +- .../internal/carpenter/Schema.kt | 10 + .../internal/carpenter/SchemaFields.kt | 8 + .../internal/amqp/GenericsTests.kt | 3 +- settings.gradle | 7 +- 349 files changed, 11114 insertions(+), 163 deletions(-) create mode 100644 buildSrc/jarfilter/README.md create mode 100644 buildSrc/jarfilter/build.gradle create mode 100644 buildSrc/jarfilter/kotlin-metadata/build.gradle create mode 100644 buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/ClassTransformer.kt create mode 100644 buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/Elements.kt create mode 100644 buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/JarFilterPlugin.kt create mode 100644 buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/JarFilterTask.kt create mode 100644 buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/KotlinAwareVisitor.kt create mode 100644 buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetaFixerTask.kt create mode 100644 buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetaFixerTransformer.kt create mode 100644 buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetaFixerVisitor.kt create mode 100644 buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetadataTransformer.kt create mode 100644 buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/Repeatable.kt create mode 100644 buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/Utils.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/AbstractFunctionTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteAndStubTests.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteConstructorTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteExtensionValPropertyTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteFieldTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteFunctionTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteLazyTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteMultiFileTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteNestedClassTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteObjectTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteSealedSubclassTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteStaticFieldTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteStaticFunctionTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteStaticValPropertyTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteStaticVarPropertyTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteTypeAliasFromFileTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteValPropertyTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteVarPropertyTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DummyJar.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/EmptyPackage.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/FieldElementTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/FieldRemovalTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/InterfaceFunctionTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/JarFilterConfigurationTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/JarFilterProject.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/JarFilterTimestampTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixAnnotationTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixConfigurationTests.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixConstructorTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixFunctionTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixNestedClassTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixPackageTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixProject.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixSealedClassTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixTimestampTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixValPropertyTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixVarPropertyTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MethodElementTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/RemoveAnnotationsTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StaticFieldRemovalTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StdOutLogging.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StubConstructorTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StubFunctionOutTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StubStaticFunctionTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StubValPropertyTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StubVarPropertyTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/Utilities.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/UtilsTest.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/annotations/Deletable.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/asm/AsmTools.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/asm/ClassMetadata.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/asm/FileMetadata.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/asm/MetadataTools.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/matcher/JavaMatchers.kt create mode 100644 buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/matcher/KotlinMatchers.kt create mode 100644 buildSrc/jarfilter/src/test/resources/abstract-function/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/abstract-function/kotlin/net/corda/gradle/AbstractFunctions.kt create mode 100644 buildSrc/jarfilter/src/test/resources/annotations/kotlin/net/corda/gradle/jarfilter/DeleteMe.kt create mode 100644 buildSrc/jarfilter/src/test/resources/annotations/kotlin/net/corda/gradle/jarfilter/RemoveMe.kt create mode 100644 buildSrc/jarfilter/src/test/resources/annotations/kotlin/net/corda/gradle/jarfilter/StubMeOut.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-and-stub/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/delete-and-stub/kotlin/net/corda/gradle/DeletePackageWithStubbed.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-and-stub/kotlin/net/corda/gradle/HasDeletedInsideStubbed.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-and-stub/kotlin/net/corda/gradle/HasPropertyForDeleteAndStub.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-constructor/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/delete-constructor/kotlin/net/corda/gradle/HasConstructorToDelete.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-constructor/kotlin/net/corda/gradle/PrimaryConstructorsToDelete.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-extension-val/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/delete-extension-val/kotlin/net/corda/gradle/HasValExtension.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-field/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/delete-field/kotlin/net/corda/gradle/HasFieldToDelete.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-file-typealias/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/delete-file-typealias/kotlin/net/corda/gradle/FileWithTypeAlias.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-function/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/delete-function/kotlin/net/corda/gradle/HasFunctionToDelete.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-function/kotlin/net/corda/gradle/HasIndirectFunctionToDelete.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-lazy/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/delete-lazy/kotlin/net/corda/gradle/HasLazy.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-multifile/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/delete-multifile/kotlin/net/corda/gradle/HasInt.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-multifile/kotlin/net/corda/gradle/HasLong.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-multifile/kotlin/net/corda/gradle/HasString.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-nested-class/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/delete-nested-class/kotlin/net/corda/gradle/HasNestedClasses.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-nested-class/kotlin/net/corda/gradle/SealedClass.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-object/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/delete-object/kotlin/net/corda/gradle/HasObjects.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-sealed-subclass/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/delete-sealed-subclass/kotlin/net/corda/gradle/SealedWithSubclasses.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-static-field/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/delete-static-field/kotlin/net/corda/gradle/StaticFieldsToDelete.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-static-function/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/delete-static-function/kotlin/net/corda/gradle/StaticFunctionsToDelete.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-static-val/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/delete-static-val/kotlin/net/corda/gradle/StaticValToDelete.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-static-var/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/delete-static-var/kotlin/net/corda/gradle/StaticVarToDelete.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-val-property/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/delete-val-property/kotlin/net/corda/gradle/HasValPropertyForDelete.kt create mode 100644 buildSrc/jarfilter/src/test/resources/delete-var-property/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/delete-var-property/kotlin/net/corda/gradle/HasVarPropertyForDelete.kt create mode 100644 buildSrc/jarfilter/src/test/resources/gradle.properties create mode 100644 buildSrc/jarfilter/src/test/resources/interface-function/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/interface-function/kotlin/net/corda/gradle/InterfaceFunctions.kt create mode 100644 buildSrc/jarfilter/src/test/resources/remove-annotations/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/remove-annotations/kotlin/net/corda/gradle/HasUnwantedAnnotations.kt create mode 100644 buildSrc/jarfilter/src/test/resources/repositories.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/settings.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/stub-constructor/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/stub-constructor/kotlin/net/corda/gradle/HasConstructorToStub.kt create mode 100644 buildSrc/jarfilter/src/test/resources/stub-constructor/kotlin/net/corda/gradle/PrimaryConstructorsToStub.kt create mode 100644 buildSrc/jarfilter/src/test/resources/stub-function/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/stub-function/kotlin/net/corda/gradle/HasFunctionToStub.kt create mode 100644 buildSrc/jarfilter/src/test/resources/stub-function/kotlin/net/corda/gradle/RuntimeAnnotations.kt create mode 100644 buildSrc/jarfilter/src/test/resources/stub-static-function/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/stub-static-function/kotlin/net/corda/gradle/StaticFunctionsToStub.kt create mode 100644 buildSrc/jarfilter/src/test/resources/stub-val-property/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/stub-val-property/kotlin/net/corda/gradle/HasValPropertyForStub.kt create mode 100644 buildSrc/jarfilter/src/test/resources/stub-var-property/build.gradle create mode 100644 buildSrc/jarfilter/src/test/resources/stub-var-property/kotlin/net/corda/gradle/HasVarPropertyForStub.kt create mode 100644 buildSrc/jarfilter/unwanteds/build.gradle create mode 100644 buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasData.kt create mode 100644 buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasUnwantedFun.kt create mode 100644 buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasUnwantedVal.kt create mode 100644 buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasUnwantedVar.kt create mode 100644 buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasVal.kt create mode 100644 buildSrc/jarfilter/unwanteds/src/main/kotlin/net/corda/gradle/unwanted/HasVar.kt create mode 100644 core-deterministic/build.gradle create mode 100644 core-deterministic/src/main/kotlin/net/corda/core/internal/ToggleField.kt create mode 100644 core-deterministic/src/main/kotlin/net/corda/core/serialization/SerializationFactory.kt create mode 100644 core-deterministic/testing/build.gradle create mode 100644 core-deterministic/testing/common/build.gradle create mode 100644 core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/LocalSerializationRule.kt create mode 100644 core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/MockContractAttachment.kt create mode 100644 core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/SampleData.kt create mode 100644 core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/TransactionVerificationRequest.kt create mode 100644 core-deterministic/testing/data/build.gradle create mode 100644 core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/GenerateData.kt create mode 100644 core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/KeyStoreGenerator.kt create mode 100644 core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/TransactionGenerator.kt create mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/CheatingSecurityProvider.kt create mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/CordaExceptionTest.kt create mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/KeyStoreProvider.kt create mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/Utilities.kt create mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/AttachmentTest.kt create mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/PrivacySaltTest.kt create mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/MerkleTreeTest.kt create mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/SecureHashTest.kt create mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/SecureRandomTest.kt create mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt create mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/transactions/TransactionWithSignaturesTest.kt create mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/EnclaveletTest.kt create mode 100644 core-deterministic/testing/src/test/resources/log4j2-test.xml create mode 100644 core/src/main/kotlin/net/corda/core/DeleteForDJVM.kt create mode 100644 core/src/main/kotlin/net/corda/core/KeepForDJVM.kt create mode 100644 core/src/main/kotlin/net/corda/core/StubOutForDJVM.kt create mode 100644 core/src/main/kotlin/net/corda/core/crypto/internal/PlatformSecureRandom.kt create mode 100644 create-jdk8u/.gitignore create mode 100644 create-jdk8u/Makefile create mode 100644 create-jdk8u/build.gradle create mode 100644 create-jdk8u/settings.gradle create mode 100644 jdk8u-deterministic/build.gradle create mode 100644 serialization-deterministic/build.gradle create mode 100644 serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/AttachmentsClassLoaderBuilder.kt create mode 100644 serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/ByteBufferStreams.kt create mode 100644 serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/DefaultWhitelist.kt create mode 100644 serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt create mode 100644 serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPStreams.kt diff --git a/build.gradle b/build.gradle index 9f0b4c784c..47eb5ee092 100644 --- a/build.gradle +++ b/build.gradle @@ -74,6 +74,7 @@ buildscript { ext.selenium_version = '3.8.1' ext.ghostdriver_version = '2.1.0' ext.eaagentloader_version = '1.0.3' + ext.proguard_version = constants.getProperty('proguardVersion') ext.jsch_version = '0.1.54' ext.commons_cli_version = '1.4' ext.protonj_version = '0.27.1' @@ -81,6 +82,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' @@ -105,6 +108,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}" @@ -143,7 +147,6 @@ targetCompatibility = 1.8 allprojects { apply plugin: 'kotlin' - apply plugin: 'java' apply plugin: 'jacoco' apply plugin: 'org.owasp.dependencycheck' apply plugin: 'kotlin-allopen' @@ -191,7 +194,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 if (System.getProperty("test.maxParallelForks") != null) { maxParallelForks = Integer.valueOf(System.getProperty("test.maxParallelForks")) @@ -254,6 +257,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() } @@ -324,7 +328,29 @@ 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-shell', 'corda-serialization', 'tools-blob-inspector', 'tools-network-bootstrapper'] + 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-shell', + 'corda-serialization', + 'corda-serialization-deterministic', + '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 c65b8aae95..1e48e27a37 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -2,7 +2,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' @@ -13,6 +30,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 c46d96de90..e493d16467 100644 --- a/buildSrc/settings.gradle +++ b/buildSrc/settings.gradle @@ -1,2 +1,5 @@ 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 5ac0a4a997..52144277de 100644 --- a/constants.properties +++ b/constants.properties @@ -1,7 +1,8 @@ -gradlePluginsVersion=4.0.20 +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..8a78dd5c51 --- /dev/null +++ b/core-deterministic/build.gradle @@ -0,0 +1,199 @@ +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/internal/*ToggleField*.class' + exclude 'net/corda/core/serialization/*SerializationFactory*.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/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/testing/build.gradle b/core-deterministic/testing/build.gradle new file mode 100644 index 0000000000..0222e0bf2b --- /dev/null +++ b/core-deterministic/testing/build.gradle @@ -0,0 +1,16 @@ +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..49f2ceb748 --- /dev/null +++ b/core-deterministic/testing/common/build.gradle @@ -0,0 +1,24 @@ +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..7b94ae6c74 --- /dev/null +++ b/core-deterministic/testing/data/build.gradle @@ -0,0 +1,29 @@ +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 468a8dac45..32d253d978 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -53,12 +53,6 @@ processSmokeTestResources { } } -buildscript { - repositories { - mavenCentral() - } -} - dependencies { testCompile "junit:junit:$junit_version" testCompile "commons-fileupload:commons-fileupload:$fileupload_version" @@ -124,7 +118,10 @@ task copyQuasarJar(type: Copy) { rename { filename -> "quasar.jar"} } -jar.finalizedBy(copyQuasarJar) +jar { + finalizedBy(copyQuasarJar) + baseName 'corda-core' +} configurations { testArtifacts.extendsFrom testRuntime @@ -155,10 +152,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 9037505bb2..76dc0feb69 100644 --- a/core/src/main/java/net/corda/core/crypto/Base58.java +++ b/core/src/main/java/net/corda/core/crypto/Base58.java @@ -1,5 +1,6 @@ package net.corda.core.crypto; +import net.corda.core.KeepForDJVM; import java.math.*; import java.util.*; @@ -27,6 +28,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 93c9549685..2e87a8957b 100644 --- a/core/src/main/java/net/corda/core/flows/IdentifiableException.java +++ b/core/src/main/java/net/corda/core/flows/IdentifiableException.java @@ -1,11 +1,14 @@ 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 409944fd23..049bb60c1c 100644 --- a/core/src/main/kotlin/net/corda/core/ClientRelevantError.kt +++ b/core/src/main/kotlin/net/corda/core/ClientRelevantError.kt @@ -6,4 +6,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 52bbd82172..7b5f822452 100644 --- a/core/src/main/kotlin/net/corda/core/CordaException.kt +++ b/core/src/main/kotlin/net/corda/core/CordaException.kt @@ -4,6 +4,7 @@ import net.corda.core.serialization.CordaSerializable import java.util.* @CordaSerializable +@KeepForDJVM interface CordaThrowable { var originalExceptionClassName: String? val originalMessage: String? @@ -12,6 +13,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 { @@ -59,6 +61,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 eb13b5b3c5..4b3cbf44c4 100644 --- a/core/src/main/kotlin/net/corda/core/CordaInternal.kt +++ b/core/src/main/kotlin/net/corda/core/CordaInternal.kt @@ -1,11 +1,14 @@ 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 0292c940b6..644ade5fef 100644 --- a/core/src/main/kotlin/net/corda/core/CordaOID.kt +++ b/core/src/main/kotlin/net/corda/core/CordaOID.kt @@ -4,6 +4,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 93aca8e49a..d225f66e92 100644 --- a/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt +++ b/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt @@ -1,5 +1,4 @@ @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 6a86741c6f..64cdee0792 100644 --- a/core/src/main/kotlin/net/corda/core/context/InvocationContext.kt +++ b/core/src/main/kotlin/net/corda/core/context/InvocationContext.kt @@ -1,5 +1,7 @@ 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 @@ -21,36 +23,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) } @@ -64,6 +72,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) { @@ -75,6 +84,7 @@ data class Actor(val id: Id, val serviceId: AuthServiceId, val owningLegalIdenti /** * Actor id. */ + @KeepForDJVM @CordaSerializable data class Id(val value: String) } @@ -82,6 +92,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 { /** @@ -129,5 +140,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 d06e1c59c8..281d7fae28 100644 --- a/core/src/main/kotlin/net/corda/core/context/Trace.kt +++ b/core/src/main/kotlin/net/corda/core/context/Trace.kt @@ -1,5 +1,6 @@ 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 @@ -16,6 +17,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) } @@ -32,6 +34,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) } @@ -49,6 +52,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 3355954ca8..d010ab5aa5 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Amount.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Amount.kt @@ -1,5 +1,6 @@ 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 @@ -36,6 +37,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 654fd51a89..66bdf08a30 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt @@ -1,5 +1,6 @@ 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 @@ -27,6 +28,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 a3465f4bf3..4a07afa1be 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt @@ -1,6 +1,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 @@ -16,6 +17,7 @@ interface AttachmentConstraint { } /** An [AttachmentConstraint] where [isSatisfiedBy] always returns true. */ +@KeepForDJVM object AlwaysAcceptAttachmentConstraint : AttachmentConstraint { override fun isSatisfiedBy(attachment: Attachment) = true } @@ -25,6 +27,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) { @@ -38,6 +41,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) { @@ -57,6 +61,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 9899bf27dd..7a19128c32 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt @@ -1,5 +1,6 @@ package net.corda.core.contracts +import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable /** @@ -9,6 +10,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 b9f3d48fc4..c932d503ad 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractState.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractState.kt @@ -1,5 +1,6 @@ package net.corda.core.contracts +import net.corda.core.KeepForDJVM import net.corda.core.identity.AbstractParty import net.corda.core.serialization.CordaSerializable @@ -11,6 +12,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 44ac7008ff..d7288b1c99 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt @@ -1,7 +1,8 @@ @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 dea478ecc6..d5f522a7e1 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt @@ -1,5 +1,6 @@ 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 @@ -20,6 +21,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 1f2c907069..84c1a3de33 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt @@ -1,7 +1,9 @@ @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 @@ -43,6 +45,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 { @@ -69,11 +72,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 @@ -110,6 +115,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 @@ -118,6 +124,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. @@ -128,6 +135,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 @@ -138,6 +146,7 @@ interface SchedulableState : ContractState { * * @return null if there is no activity to schedule. */ + @DeleteForDJVM fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity? } @@ -148,6 +157,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) { @@ -156,6 +166,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) @@ -170,6 +181,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" @@ -180,12 +192,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? @@ -200,6 +214,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 @@ -211,6 +226,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, @@ -229,6 +245,7 @@ data class CommandWithParties( * * TODO: Contract serialization is likely to change, so the annotation is likely temporary. */ +@KeepForDJVM @CordaSerializable interface Contract { /** @@ -260,6 +277,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. @@ -278,6 +296,7 @@ interface UpgradedContract : UpgradedContract { /** * A validator for the legacy (pre-upgrade) contract attachments on the transaction. @@ -295,8 +314,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 5f62e064de..f3a928ac17 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/UniqueIdentifier.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/UniqueIdentifier.kt @@ -1,5 +1,7 @@ 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.* @@ -17,7 +19,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 a970c42451..bfd85d732c 100644 --- a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt +++ b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt @@ -1,5 +1,6 @@ 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 @@ -31,6 +32,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 b91acec452..ef14f417e0 100644 --- a/core/src/main/kotlin/net/corda/core/cordapp/CordappContext.kt +++ b/core/src/main/kotlin/net/corda/core/cordapp/CordappContext.kt @@ -1,5 +1,6 @@ package net.corda.core.cordapp +import net.corda.core.DeleteForDJVM import net.corda.core.crypto.SecureHash /** @@ -15,6 +16,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 bf7864ee95..bec2d6ab59 100644 --- a/core/src/main/kotlin/net/corda/core/cordapp/CordappProvider.kt +++ b/core/src/main/kotlin/net/corda/core/cordapp/CordappProvider.kt @@ -1,5 +1,6 @@ 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 @@ -8,6 +9,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 59ae9c7766..be28292ff1 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt @@ -1,5 +1,7 @@ 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 @@ -26,6 +28,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 { @@ -143,6 +146,7 @@ class CompositeKey private constructor(val threshold: Int, children: List, ASN1Object() { init { @@ -241,6 +245,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 df7fc7c433..3e35032900 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeKeyFactory.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeKeyFactory.kt @@ -1,5 +1,6 @@ package net.corda.core.crypto +import net.corda.core.DeleteForDJVM import java.security.* import java.security.spec.InvalidKeySpecException import java.security.spec.KeySpec @@ -8,6 +9,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 b359e31991..56ececd801 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt @@ -1,5 +1,6 @@ package net.corda.core.crypto +import net.corda.core.KeepForDJVM import net.corda.core.serialization.deserialize import java.io.ByteArrayOutputStream import java.security.* @@ -8,6 +9,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 0ea62b1f0e..1175f80837 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt @@ -1,5 +1,6 @@ package net.corda.core.crypto +import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable /** @@ -7,6 +8,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 5b195cf52f..3cffa20584 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt @@ -1,25 +1,37 @@ package net.corda.core.crypto +import net.corda.core.KeepForDJVM import net.corda.core.crypto.CordaObjectIdentifier.COMPOSITE_KEY import net.corda.core.crypto.CordaObjectIdentifier.COMPOSITE_SIGNATURE import org.bouncycastle.asn1.ASN1ObjectIdentifier +import net.corda.core.StubOutForDJVM import java.security.Provider +@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) } } +/** + * 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 43c7c0544e..a172f31747 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -1,5 +1,7 @@ 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 @@ -58,6 +60,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. @@ -613,6 +616,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)) @@ -623,6 +627,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 { @@ -789,6 +794,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) { @@ -804,6 +810,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 e7f7898dce..0f6b87e6d8 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -1,7 +1,10 @@ +@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 @@ -124,6 +127,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() /** @@ -132,6 +136,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) /** @@ -168,6 +173,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) } @@ -189,6 +195,7 @@ fun secureRandomBytes(numOfBytes: Int): ByteArray = ByteArray(numOfBytes).apply * 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() @@ -196,6 +203,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 7bd11ec661..24e4ce1587 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt @@ -1,5 +1,6 @@ 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 @@ -11,8 +12,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 73359726a8..8f32a0e8c3 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/MerkleTree.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/MerkleTree.kt @@ -1,5 +1,6 @@ package net.corda.core.crypto +import net.corda.core.KeepForDJVM import java.util.* /** @@ -14,8 +15,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 002a357ced..151f00b950 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/NullKeys.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/NullKeys.kt @@ -1,9 +1,11 @@ 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 5ba48577dd..1ef11c341b 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt @@ -1,10 +1,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") @@ -42,6 +44,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) { /** @@ -53,9 +56,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 78ccc8b3ca..91b1e5ce55 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt @@ -1,5 +1,8 @@ +@file:KeepForDJVM 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.OpaqueBytes import net.corda.core.utilities.parseAsHex @@ -10,6 +13,7 @@ import java.security.MessageDigest * 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). */ @@ -77,6 +81,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 124794b730..cfe0ec96a8 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SignableData.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SignableData.kt @@ -1,5 +1,6 @@ package net.corda.core.crypto +import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable /** @@ -12,4 +13,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 99335bde4c..6c8d9c33e6 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SignatureMetadata.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SignatureMetadata.kt @@ -1,5 +1,6 @@ package net.corda.core.crypto +import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable /** @@ -12,4 +13,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 6c1f87ed68..c14568f9fa 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt @@ -1,5 +1,6 @@ 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 @@ -20,6 +21,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 7e17b7b295..9a914405e6 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SignedData.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SignedData.kt @@ -1,5 +1,6 @@ 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 @@ -14,6 +15,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 29d0a0d212..a26139db1d 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt @@ -1,5 +1,6 @@ package net.corda.core.crypto +import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable import java.security.InvalidKeyException import java.security.PublicKey @@ -15,6 +16,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 new file mode 100644 index 0000000000..755569df0d --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/crypto/internal/PlatformSecureRandom.kt @@ -0,0 +1,18 @@ +@file:JvmName("PlatformSecureRandom") +@file:DeleteForDJVM +package net.corda.core.crypto.internal + +import net.corda.core.DeleteForDJVM +import org.apache.commons.lang.SystemUtils +import java.security.SecureRandom + +/** + * This has been migrated into a separate class so that it + * is easier to delete from the core-deterministic module. + */ +internal val platformSecureRandom: () -> SecureRandom = when { + SystemUtils.IS_OS_LINUX -> { + { SecureRandom.getInstance("NativePRNGNonBlocking") } + } + else -> SecureRandom::getInstanceStrong +} 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 2981ebd65e..010d894453 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 @@ -1,5 +1,6 @@ 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 @@ -7,14 +8,12 @@ import net.corda.core.crypto.Crypto.decodePublicKey import net.corda.core.internal.X509EdDSAEngine import net.i2p.crypto.eddsa.EdDSAEngine import net.i2p.crypto.eddsa.EdDSASecurityProvider -import org.apache.commons.lang.SystemUtils import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.asn1.pkcs.PrivateKeyInfo import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.jcajce.provider.util.AsymmetricKeyInfoConverter import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider -import java.security.SecureRandom import java.security.Security internal val cordaSecurityProvider = CordaSecurityProvider().also { @@ -45,9 +44,6 @@ internal val bouncyCastlePQCProvider = BouncyCastlePQCProvider().apply { // i.e. if someone removes a Provider and then he/she adds a new one with the same name. // The val is private to avoid any harmful state changes. internal val providerMap = listOf(cordaBouncyCastleProvider, cordaSecurityProvider, bouncyCastlePQCProvider).map { it.name to it }.toMap() -internal val platformSecureRandomFactory: () -> SecureRandom = when { - SystemUtils.IS_OS_LINUX -> { - { SecureRandom.getInstance("NativePRNGNonBlocking") } - } - else -> SecureRandom::getInstanceStrong -} + +@DeleteForDJVM +internal fun platformSecureRandomFactory() = platformSecureRandom() // To minimise diff of CryptoUtils against open-source. \ No newline at end of file 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 30f0d9599a..98956e1c24 100644 --- a/core/src/main/kotlin/net/corda/core/flows/StateMachineRunId.kt +++ b/core/src/main/kotlin/net/corda/core/flows/StateMachineRunId.kt @@ -1,5 +1,6 @@ package net.corda.core.flows +import net.corda.core.DeleteForDJVM import net.corda.core.serialization.CordaSerializable import java.util.* @@ -10,6 +11,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 086d06c6c9..9e4ae47a0a 100644 --- a/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt +++ b/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt @@ -1,5 +1,6 @@ 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 @@ -9,6 +10,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 3215129d3f..a304ef1595 100644 --- a/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt +++ b/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt @@ -1,6 +1,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 @@ -30,6 +31,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 9316391ae7..828128bf5f 100644 --- a/core/src/main/kotlin/net/corda/core/identity/Party.kt +++ b/core/src/main/kotlin/net/corda/core/identity/Party.kt @@ -1,5 +1,6 @@ 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 @@ -26,6 +27,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 60d11c613f..a282390c88 100644 --- a/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt +++ b/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt @@ -1,5 +1,6 @@ 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 @@ -13,6 +14,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 1e05c8c4c2..b86890eb70 100644 --- a/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt +++ b/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt @@ -1,5 +1,8 @@ +@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 @@ -23,8 +26,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 7b3283b28a..231550b26f 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt @@ -1,10 +1,12 @@ 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 4d7d8b9dcd..7fc84f3e87 100644 --- a/core/src/main/kotlin/net/corda/core/internal/Emoji.kt +++ b/core/src/main/kotlin/net/corda/core/internal/Emoji.kt @@ -1,8 +1,11 @@ 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 0f159be31f..2c70cd54d1 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FlowIORequest.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FlowIORequest.kt @@ -1,5 +1,6 @@ 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 @@ -11,6 +12,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 fc6a5a529e..1cdf50f088 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt @@ -4,10 +4,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 a00be517b0..a5fda08f08 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -1,9 +1,11 @@ @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 @@ -108,6 +110,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 @@ -123,7 +126,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() } @@ -154,6 +157,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) @@ -161,6 +165,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) @@ -169,6 +174,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() @@ -181,6 +187,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() @@ -208,6 +215,7 @@ fun ByteArrayOutputStream.toInputStreamAndHash(): InputStreamAndHash { return InputStreamAndHash(bytes.inputStream(), bytes.sha256()) } +@KeepForDJVM data class InputStreamAndHash(val inputStream: InputStream, val sha256: SecureHash.SHA256) { companion object { /** @@ -215,6 +223,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() @@ -268,24 +277,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 */ @@ -314,6 +328,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 @@ -370,12 +385,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 @@ -391,12 +406,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 @@ -409,20 +426,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 196342194f..bc2d0132a6 100644 --- a/core/src/main/kotlin/net/corda/core/internal/LazyPool.kt +++ b/core/src/main/kotlin/net/corda/core/internal/LazyPool.kt @@ -1,5 +1,6 @@ package net.corda.core.internal +import net.corda.core.DeleteForDJVM import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.Semaphore @@ -14,6 +15,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 5d6a156803..52ec2226ae 100644 --- a/core/src/main/kotlin/net/corda/core/internal/LazyStickyPool.kt +++ b/core/src/main/kotlin/net/corda/core/internal/LazyStickyPool.kt @@ -1,5 +1,6 @@ package net.corda.core.internal +import net.corda.core.DeleteForDJVM import java.util.* import java.util.concurrent.LinkedBlockingQueue @@ -11,6 +12,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 9f97612852..db15d33356 100644 --- a/core/src/main/kotlin/net/corda/core/internal/LegalNameValidator.kt +++ b/core/src/main/kotlin/net/corda/core/internal/LegalNameValidator.kt @@ -1,5 +1,6 @@ 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 @@ -105,12 +106,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() @@ -121,6 +124,7 @@ object LegalNameValidator { } } + @KeepForDJVM private class CharacterRule(vararg val bannedChars: Char) : Rule() { override fun validate(legalName: String) { bannedChars.forEach { @@ -129,6 +133,7 @@ object LegalNameValidator { } } + @KeepForDJVM private class WordRule(vararg val bannedWords: String) : Rule() { override fun validate(legalName: String) { bannedWords.forEach { @@ -137,12 +142,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() @@ -150,6 +157,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. @@ -157,6 +165,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 96786ea3e9..da6e334f27 100644 --- a/core/src/main/kotlin/net/corda/core/internal/LifeCycle.kt +++ b/core/src/main/kotlin/net/corda/core/internal/LifeCycle.kt @@ -1,5 +1,6 @@ package net.corda.core.internal +import net.corda.core.DeleteForDJVM import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.withLock @@ -9,6 +10,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 00c5a56d70..8413caa3b1 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt @@ -1,6 +1,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 @@ -22,6 +23,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() { @@ -38,6 +40,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 eedd576694..b19439710b 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ThreadBox.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ThreadBox.kt @@ -1,5 +1,6 @@ package net.corda.core.internal +import net.corda.core.DeleteForDJVM import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -21,6 +22,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 87ae81d527..5c0d6ebe80 100644 --- a/core/src/main/kotlin/net/corda/core/internal/X509EdDSAEngine.kt +++ b/core/src/main/kotlin/net/corda/core/internal/X509EdDSAEngine.kt @@ -1,5 +1,6 @@ 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.* @@ -24,6 +25,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 d4092478cf..cf8ddc85a0 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 @@ -1,5 +1,6 @@ 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 @@ -10,6 +11,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 e8dabb91ce..62e5e2f1f6 100644 --- a/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt @@ -1,5 +1,6 @@ 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 @@ -11,6 +12,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 11970f62f4..05cabdbed1 100644 --- a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt +++ b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt @@ -1,5 +1,6 @@ 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 @@ -23,6 +24,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, @@ -100,5 +102,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 30badc8807..8c304d81d2 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -1,5 +1,6 @@ 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 @@ -22,6 +23,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 { /** @@ -93,6 +95,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 395e5cbccb..0ab6627f0e 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 @@ -1,5 +1,6 @@ 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 @@ -58,7 +59,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 a4eeb0b487..b7bf3611fa 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 @@ -1,5 +1,6 @@ package net.corda.core.node.services +import net.corda.core.DeleteForDJVM import net.corda.core.contracts.TimeWindow import java.time.Clock @@ -7,6 +8,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 b04c96729f..5bdb494be6 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 @@ -1,5 +1,6 @@ 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 @@ -10,6 +11,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 d72eec72f2..e1c731ce86 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 @@ -1,5 +1,6 @@ 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 @@ -9,6 +10,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 b7606475f3..1865a2e61c 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 @@ -1,6 +1,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.* @@ -177,6 +178,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 fccbbf1ffb..6e6e7ea34f 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt @@ -1,5 +1,6 @@ package net.corda.core.schemas +import net.corda.core.KeepForDJVM import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateRef import net.corda.core.serialization.CordaSerializable @@ -15,6 +16,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. @@ -37,6 +39,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>) { @@ -69,6 +72,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 @@ -76,6 +80,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) @@ -90,8 +95,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 0799e9a270..a3b137227d 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsException.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsException.kt @@ -1,8 +1,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 82755a9efd..7e92f3cf15 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt @@ -1,7 +1,10 @@ +@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 @@ -107,6 +110,7 @@ interface SerializationEncoding /** * Parameters to serialization and deserialization. */ +@KeepForDJVM @DoNotImplement interface SerializationContext { /** @@ -156,6 +160,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 /** @@ -183,6 +188,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 } } @@ -191,6 +197,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 } @@ -198,13 +205,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 } /** @@ -244,6 +252,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) @@ -262,6 +271,7 @@ fun T.serialize(serializationFactory: SerializationFactory = Serializa * to get the original object back. */ @Suppress("unused") +@KeepForDJVM class SerializedBytes(bytes: ByteArray) : OpaqueBytes(bytes) { companion object { /** @@ -285,10 +295,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 05852302ce..d0c910b638 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomSerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomSerializer.kt @@ -1,5 +1,7 @@ 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 @@ -9,6 +11,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 9ec9fb75c0..1bc11091f3 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationToken.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationToken.kt @@ -1,5 +1,6 @@ package net.corda.core.serialization +import net.corda.core.DeleteForDJVM import net.corda.core.node.ServiceHub import net.corda.core.serialization.SingletonSerializationToken.Companion.singletonSerializationToken @@ -18,6 +19,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 @@ -26,6 +28,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 } @@ -33,6 +36,7 @@ interface SerializationToken { /** * A context for mapping SerializationTokens to/from SerializeAsTokens. */ +@DeleteForDJVM interface SerializeAsTokenContext { val serviceHub: ServiceHub fun putSingleton(toBeTokenized: SerializeAsToken) @@ -43,6 +47,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) @@ -58,6 +63,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 0c2be0c16b..cc0b56e32e 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationWhitelist.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationWhitelist.kt @@ -1,10 +1,13 @@ 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 a8cbfd5724..28c6ad7900 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 @@ -1,5 +1,7 @@ +@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 @@ -7,6 +9,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 @@ -16,6 +19,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 14483543b2..16580cfdd4 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt @@ -1,6 +1,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 @@ -11,6 +12,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 2af0347e8c..a019a31068 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt @@ -1,5 +1,6 @@ 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 @@ -22,6 +23,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( /** @@ -110,6 +112,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. */ @@ -158,6 +161,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 1f86e50b27..bf3deadcbb 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -1,5 +1,6 @@ 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 @@ -26,6 +27,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. */ @@ -58,10 +60,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 } @@ -237,6 +243,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 b61daf9963..c484781d19 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt @@ -1,6 +1,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 @@ -107,6 +108,7 @@ abstract class TraversableTransaction(open val componentGroups: List, val nonces: List, val partialMerkleTree: PartialMerkleTree) : ComponentGroup(groupIndex, components) { init { @@ -342,6 +345,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") @@ -349,5 +353,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 973de9e942..209d312e19 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt @@ -1,5 +1,6 @@ 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 @@ -11,6 +12,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 fbee73d680..80667b5286 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt @@ -1,5 +1,7 @@ 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 @@ -21,6 +23,7 @@ import java.security.PublicKey * on the fly. */ @CordaSerializable +@KeepForDJVM data class NotaryChangeWireTransaction( /** * Contains all of the transaction components in serialized form. @@ -61,12 +64,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 { @@ -82,6 +87,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 94a993e328..440163f0e0 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -2,6 +2,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 @@ -33,6 +35,7 @@ import java.util.function.Predicate * sign. */ // DOCSTART 1 +@KeepForDJVM @CordaSerializable data class SignedTransaction(val txBits: SerializedBytes, override val sigs: List @@ -131,6 +134,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 @@ -159,6 +163,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) { @@ -169,6 +174,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() @@ -176,6 +182,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() @@ -185,6 +192,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. @@ -195,6 +203,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) @@ -210,6 +219,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) @@ -224,6 +234,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}") @@ -234,12 +245,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}") @@ -253,6 +266,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 12808fc299..8d012e2ea9 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -1,6 +1,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 @@ -33,6 +34,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 77ceb3840b..237abc3e35 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt @@ -1,6 +1,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 @@ -12,6 +13,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 c9c56f2a17..6f4a63b898 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -1,6 +1,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.* @@ -41,9 +43,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>, @@ -85,6 +91,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) }, @@ -254,6 +261,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 5eb9e54520..1d5ceb3f73 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt @@ -1,7 +1,9 @@ @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 @@ -19,6 +21,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 @@ -142,6 +145,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 { /** @@ -187,6 +191,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 902bc8e9bd..c2cf1c151d 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt @@ -1,7 +1,9 @@ @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 5020a08700..a68bfb2d49 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/Id.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/Id.kt @@ -1,5 +1,6 @@ package net.corda.core.utilities +import net.corda.core.DeleteForDJVM import java.time.Instant import java.time.Instant.now @@ -15,6 +16,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 b53bdc9f34..240edbfd1e 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt @@ -1,5 +1,8 @@ +@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 @@ -125,6 +128,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 ab5f0b86d7..06fb7834d6 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/NonEmptySet.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/NonEmptySet.kt @@ -1,5 +1,6 @@ package net.corda.core.utilities +import net.corda.core.KeepForDJVM import java.util.* import java.util.function.Consumer import java.util.stream.Stream @@ -7,6 +8,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 cfe471c1b1..7d2d678e5f 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/Try.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/Try.kt @@ -1,5 +1,6 @@ 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 @@ -59,6 +60,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 @@ -66,6 +68,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 bb693e99c0..94fcb2a4cf 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/UntrustworthyData.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/UntrustworthyData.kt @@ -1,10 +1,9 @@ +@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 /** @@ -18,11 +17,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 e851dae583..72beb0d624 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/UuidGenerator.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/UuidGenerator.kt @@ -1,7 +1,9 @@ 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..30e46cd17f --- /dev/null +++ b/create-jdk8u/build.gradle @@ -0,0 +1,159 @@ +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..c6e8eefe7d --- /dev/null +++ b/create-jdk8u/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'deterministic-rt' diff --git a/docs/source/deterministic-modules.rst b/docs/source/deterministic-modules.rst index 435b75c7fc..35a579d3da 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 @@ -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``, @@ -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/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 110bf1ebb9..4abb273b48 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 ec3046c682..e7b94b9635 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 @@ -7,6 +7,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 @@ -460,6 +461,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/serialization-deterministic/build.gradle b/serialization-deterministic/build.gradle new file mode 100644 index 0000000000..ccb1c32558 --- /dev/null +++ b/serialization-deterministic/build.gradle @@ -0,0 +1,186 @@ +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 36516d7966..8493821fb4 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/AllButBlacklisted.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/AllButBlacklisted.kt @@ -1,5 +1,6 @@ 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 @@ -34,6 +35,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 9a7a749e32..79de2f342b 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/AttachmentsClassLoader.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/AttachmentsClassLoader.kt @@ -1,5 +1,6 @@ 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 @@ -22,6 +23,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 e8dc6b26af..5df12028a9 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/ClientContexts.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/ClientContexts.kt @@ -1,7 +1,8 @@ +@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 2cf38ce721..620544181f 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/GeneratedAttachment.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/GeneratedAttachment.kt @@ -1,8 +1,10 @@ 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 a1d4e5e04b..cc8082bb5b 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/OrdinalIO.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/OrdinalIO.kt @@ -1,10 +1,12 @@ 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 @@ -19,6 +21,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 caf508738c..d275643fc3 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationFormat.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationFormat.kt @@ -1,5 +1,6 @@ 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 @@ -12,6 +13,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? { @@ -19,6 +21,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, @@ -34,6 +37,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 89739f6988..f37091df49 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt @@ -2,6 +2,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 @@ -20,6 +22,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, @@ -64,9 +67,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() @@ -90,10 +94,12 @@ internal class AttachmentsClassLoaderBuilder(private val properties: Map, SerializationScheme> ) : SerializationFactory() { + @DeleteForDJVM constructor() : this(ConcurrentHashMap()) companion object { @@ -155,6 +161,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 1cffdbb9a1..d028e91168 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializeAsTokenContextImpl.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializeAsTokenContextImpl.kt @@ -1,5 +1,7 @@ +@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 @@ -19,6 +21,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 4070d70e10..d19a177d2a 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/ServerContexts.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/ServerContexts.kt @@ -1,7 +1,8 @@ @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 5f0143e002..c2d12e8b55 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/SharedContexts.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/SharedContexts.kt @@ -1,5 +1,9 @@ +@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.amqp.amqpMagic @@ -13,6 +17,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 5136ebe23f..ebe030b81d 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/UseCaseAwareness.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/UseCaseAwareness.kt @@ -1,5 +1,7 @@ +@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 e5065ea06d..6a38e9c0f1 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 @@ -3,6 +3,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 @@ -31,11 +34,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 @@ -63,6 +68,7 @@ abstract class AbstractAMQPSerializationScheme( } } + @DeleteForDJVM val List.customSerializers get() = flatMap { it.serializationCustomSerializers }.toSet() } @@ -126,6 +132,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 283b997a84..b4b0b2e58e 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 @@ -1,5 +1,6 @@ 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 @@ -8,6 +9,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 c861e22f42..c249a98aed 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 @@ -1,7 +1,8 @@ @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 3caac111d9..061f8e7d4f 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 @@ -1,5 +1,6 @@ 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 @@ -13,6 +14,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 9318da28f4..1a5c5b48b3 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 @@ -1,5 +1,6 @@ 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 @@ -14,6 +15,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 822bb3d855..6edf3e3e00 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 @@ -1,5 +1,6 @@ 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 @@ -27,6 +28,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 74531364a6..ada0e32823 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 @@ -1,6 +1,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 @@ -11,6 +12,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 a74bd82675..37be145720 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 @@ -1,5 +1,6 @@ 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 @@ -13,6 +14,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 d5a71f5ac4..79e576c0bb 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 @@ -1,5 +1,6 @@ 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 @@ -43,6 +44,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 @@ -259,6 +261,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 188ce947a2..39fce872fb 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 @@ -2,6 +2,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 @@ -12,6 +13,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 @@ -28,6 +30,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 2cecfa492e..26e9e7f090 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 @@ -1,5 +1,7 @@ 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 @@ -15,6 +17,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) @@ -130,6 +133,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 fe052c5b3d..e83c5c4119 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 @@ -1,5 +1,6 @@ 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 @@ -60,6 +61,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 a89b1d0880..4504eca85c 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 @@ -1,5 +1,6 @@ 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 @@ -17,6 +18,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 @@ -55,6 +57,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 1c6156f818..8409c99a8c 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 @@ -1,5 +1,6 @@ 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 @@ -18,6 +19,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 @@ -46,6 +48,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)) @@ -86,6 +89,7 @@ data class Descriptor(val name: Symbol?, val code: UnsignedLong? = null) : Descr } } +@KeepForDJVM data class Field( val name: String, val type: String, @@ -153,6 +157,7 @@ sealed class TypeNotation : DescribedType { abstract val descriptor: Descriptor } +@KeepForDJVM data class CompositeType( override val name: String, override val label: String?, @@ -203,6 +208,7 @@ data class CompositeType( } } +@KeepForDJVM data class RestrictedType(override val name: String, override val label: String?, override val provides: List, @@ -253,6 +259,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 @@ -282,6 +289,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 36bc35d147..65b07d2638 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 @@ -2,6 +2,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 @@ -87,6 +88,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 531c025a3b..74e9d6b1d0 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 @@ -1,5 +1,6 @@ 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 @@ -13,6 +14,7 @@ import java.lang.reflect.Type import java.util.* import kotlin.collections.LinkedHashSet +@KeepForDJVM data class BytesAndSchemas( val obj: SerializedBytes, val schema: Schema, @@ -24,6 +26,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 560ddb998d..32c958d0d2 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 @@ -2,6 +2,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 @@ -17,7 +20,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) /** @@ -38,6 +43,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, @@ -48,6 +54,7 @@ open class SerializerFactory( val serializersByDescriptor: MutableMap>, private val customSerializers: MutableList, val transformsCache: MutableMap>>) { + @DeleteForDJVM constructor(whitelist: ClassWhitelist, classCarpenter: ClassCarpenter, evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), @@ -58,6 +65,7 @@ open class SerializerFactory( customSerializers = CopyOnWriteArrayList(), transformsCache = ConcurrentHashMap()) + @DeleteForDJVM constructor(whitelist: ClassWhitelist, classLoader: ClassLoader, evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), @@ -274,6 +282,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 e93a519133..8c2bd34086 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 @@ -1,5 +1,6 @@ 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 @@ -15,6 +16,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 8714d49206..a36c56c374 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 @@ -1,5 +1,6 @@ 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 @@ -20,6 +21,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 e922136ee1..100fe70fb0 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 @@ -1,5 +1,6 @@ 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 @@ -303,6 +304,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 ece4cf29ac..05fe559ac7 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 @@ -1,5 +1,6 @@ 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 @@ -29,5 +30,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 e7c9d76147..79be3f97e7 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 @@ -1,5 +1,6 @@ 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 @@ -12,5 +13,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 236c3d01be..6a3ae66b81 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 @@ -1,5 +1,6 @@ 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 @@ -12,5 +13,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 4a71edf700..135e93710c 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 @@ -2,11 +2,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 { @@ -88,5 +90,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 4fbc1a7ddf..f2c16d009d 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 @@ -1,5 +1,6 @@ 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 @@ -12,5 +13,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 7f25bef8f5..7545d97c47 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 @@ -1,6 +1,9 @@ +@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 @@ -22,6 +25,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) @@ -46,6 +50,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 @@ -96,6 +101,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 4d5ccfb864..ac1346b053 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 @@ -1,6 +1,7 @@ package net.corda.serialization.internal.carpenter import net.corda.core.CordaRuntimeException +import net.corda.core.DeleteForDJVM import org.objectweb.asm.Type /** @@ -16,6 +17,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 e15e69e2a0..c85bb6ebd6 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 @@ -1,5 +1,8 @@ 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 @@ -23,6 +26,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>>, @@ -47,7 +51,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) } @@ -62,6 +67,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>() @@ -91,6 +97,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()) { @@ -104,6 +111,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 3685cacf8c..8690668367 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 @@ -1,9 +1,13 @@ +@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 } @@ -16,6 +20,7 @@ enum class SchemaFlags { * - [InterfaceSchema] * - [EnumSchema] */ +@KeepForDJVM abstract class Schema( val name: String, var fields: Map, @@ -40,6 +45,7 @@ abstract class Schema( fun descriptorsIncludingSuperclasses(): Map = (superclass?.descriptorsIncludingSuperclasses() ?: emptyMap()) + fields.descriptors() + @DeleteForDJVM abstract fun generateFields(cw: ClassWriter) val jvmName: String @@ -64,6 +70,7 @@ fun EnumMap.simpleFieldAccess(): Boolean { /** * Represents a concrete object. */ +@DeleteForDJVM class ClassSchema( name: String, fields: Map, @@ -79,6 +86,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, @@ -93,6 +101,7 @@ class InterfaceSchema( /** * Represents an enumerated type. */ +@DeleteForDJVM class EnumSchema( name: String, fields: Map @@ -114,6 +123,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 2cd35385a3..c9babf9348 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 @@ -1,6 +1,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 @@ -15,7 +16,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) } @@ -26,6 +29,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) @@ -59,6 +63,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;" @@ -89,6 +94,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;" @@ -110,6 +116,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 @@ -130,6 +137,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 e7c24304c6..a99fefd455 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 @@ -12,6 +12,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 @@ -28,7 +29,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 0e31eeb1fb..e2107da0bc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,6 +5,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' @@ -22,6 +26,7 @@ include 'experimental:sandbox' include 'experimental:quasar-hook' include 'experimental:kryo-hook' include 'experimental:corda-utils' +include 'jdk8u-deterministic' include 'test-common' include 'test-utils' include 'smoke-test-utils' @@ -53,4 +58,4 @@ include 'samples:notary-demo' include 'samples:bank-of-corda-demo' include 'samples:cordapp-configuration' include 'serialization' - +include 'serialization-deterministic' From 5cc52d10fb434c8df41d495c5cc262b60fe6aa45 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Tue, 12 Jun 2018 08:50:11 +0100 Subject: [PATCH 7/8] ENT-1463, ENT-1903: Fix references in the documentation. (#3346) --- docs/source/deterministic-modules.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/deterministic-modules.rst b/docs/source/deterministic-modules.rst index 35a579d3da..f6327f1649 100644 --- a/docs/source/deterministic-modules.rst +++ b/docs/source/deterministic-modules.rst @@ -118,7 +118,7 @@ in order to maintain the deterministic JARs. .. sourcecode:: kotlin - @file:Deterministic + @file:KeepForDJVM package net.corda.core.internal .. @@ -144,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) From 8087f3c5d3c823a5fbda9a728f129b0925c1fddb Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Tue, 12 Jun 2018 08:50:26 +0100 Subject: [PATCH 8/8] CORDA-1494: Update changelog (#3344) --- docs/source/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index f673e74ee8..d298dfa51b 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -36,6 +36,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.