From 688639e97fb10adebbe1917c3066eb262d44ceda Mon Sep 17 00:00:00 2001 From: Sam Gammon Date: Wed, 27 Mar 2024 23:51:29 -0700 Subject: [PATCH] feat(java): classfile and jar readers (#49) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(java): classfile and jar readers - feat(java): reader facade for classfiles - feat(java): support for reading jpms module classes - feat(java): support for reading jars - test(java): compile harness - test(java): jar assemble harness - test(java): full test coverage for java package - test(java); inline testsuite for class parser - chore(java): rewrite much of class parser as typescript - chore: bump packages for eventual release → `1.0.2` Not yet finished: JAR reader. Signed-off-by: Sam Gammon * chore: run formatter Signed-off-by: Sam Gammon * feat(java): implement manifest reader Signed-off-by: Sam Gammon * feat(java): implement jar file support and tests Signed-off-by: Sam Gammon * chore: sync lockfiles, general cleanup Signed-off-by: Sam Gammon * fix: bad prettier run Signed-off-by: Sam Gammon --------- Signed-off-by: Sam Gammon --- .github/CODE_OF_CONDUCT.md | 67 +- .github/GOVERNANCE.md | 4 +- .github/SECURITY.md | 20 +- .github/codecov.yml | 26 +- .github/dependency-review-config.yml | 8 +- .github/pr-badge.yml | 16 +- .github/workflows/check.dependency-review.yml | 10 +- .github/workflows/check.gradle-wrapper.yml | 10 +- .github/workflows/check.hashes.yml | 10 +- .github/workflows/ci.build-test.yml | 44 +- .github/workflows/ci.dependency-graph.yml | 22 +- .github/workflows/ci.publish-package.yml | 90 +- .github/workflows/ci.publish-packages.yml | 46 +- .github/workflows/deploy.site.yml | 28 +- .github/workflows/on.pr.yml | 14 +- .github/workflows/on.push.yml | 12 +- .github/workflows/on.queue.yml | 10 +- .github/workflows/on.release.yml | 8 +- .prettierignore | 2 + .prettierrc | 18 +- CONTRIBUTING.md | 24 +- README.md | 64 +- _config.yml | 9 +- about.markdown | 9 +- assets/css/style.scss | 2 +- bun.lockb | Bin 269848 -> 277312 bytes package.json | 58 +- packages/README.md | 6 +- packages/gradle/README.md | 6 +- packages/gradle/gradle-constants.ts | 2 +- packages/gradle/gradle-facade.ts | 42 +- packages/gradle/gradle-model.ts | 96 +- packages/gradle/gradle-schema.ts | 88 +- packages/gradle/gradle-util.ts | 58 +- packages/gradle/index.mts | 6 +- packages/gradle/package.json | 76 +- packages/gradle/tests/gradle-facade.test.ts | 212 ++-- packages/gradle/tests/gradle-model.test.ts | 72 +- packages/gradle/tests/gradle-schema.test.ts | 42 +- packages/gradle/tests/sanity.test.ts | 18 - packages/indexer/indexer-model.mts | 54 +- packages/indexer/indexer.mts | 105 +- packages/indexer/package.json | 98 +- packages/indexer/tests/sanity.test.ts | 8 +- packages/java/README.md | 6 +- packages/java/classfile-parser.ts | 79 -- packages/java/index.mts | 2 +- packages/java/java-classfile.ts | 338 ++++++ packages/java/{javahome.ts => java-home.ts} | 145 ++- packages/java/java-jar.ts | 1009 +++++++++++++++++ packages/java/java-manifest.ts | 556 +++++++++ packages/java/java-model.ts | 224 +++- .../attribute.ts} | 9 +- packages/java/javaclasses/constant-pool.ts | 55 + packages/java/javaclasses/constant-type.ts | 2 +- .../java/javaclasses/instruction-parser.ts | 352 ++---- .../java/javaclasses/java-class-reader.ts | 845 +++++++------- packages/java/javaclasses/java-class-types.ts | 102 ++ packages/java/javaclasses/modifier.ts | 4 +- packages/java/javaclasses/opcode.ts | 4 +- packages/java/javamodules/jdk-modules.ts | 88 ++ packages/java/javamodules/module-flags.ts | 57 + packages/java/package.json | 83 +- packages/java/tests/classfile-parser.test.ts | 21 - packages/java/tests/compiler/Hello.java | 7 + .../java/tests/compiler/complex/Another.java | 7 + .../java/tests/compiler/complex/Hello.java | 7 + .../compiler/complex/Implementation.java | 5 + .../complex/META-INF/services/hello.Service | 0 .../java/tests/compiler/complex/Service.java | 5 + .../tests/compiler/complex/module-info.java | 15 + packages/java/tests/compiler/module-info.java | 5 + packages/java/tests/compiler/open/Hello.java | 7 + .../java/tests/compiler/open/module-info.java | 4 + .../java/tests/java-classfile-parser.test.ts | 929 +++++++++++++++ packages/java/tests/java-classfile.test.ts | 542 +++++++++ packages/java/tests/java-home.test.ts | 42 +- packages/java/tests/java-jar.test.ts | 314 +++++ packages/java/tests/java-manifest.test.ts | 298 +++++ packages/java/tests/java-toolchain.test.ts | 221 ++++ packages/java/tests/sanity.test.ts | 18 - packages/java/tests/testutil.test.ts | 364 ++++++ packages/java/toolchain/abstract.ts | 307 ++++- packages/java/toolchain/compiler.ts | 43 +- packages/java/toolchain/index.ts | 5 +- packages/java/toolchain/launcher.ts | 43 +- packages/java/toolchain/repositories.ts | 123 ++ packages/java/toolchain/tool.ts | 58 + packages/maven/README.md | 6 +- packages/maven/index.mts | 2 +- packages/maven/maven-constants.ts | 2 +- packages/maven/maven-model.ts | 134 +-- packages/maven/maven-parser.ts | 110 +- packages/maven/maven-schema.ts | 98 +- packages/maven/package.json | 84 +- packages/maven/tests/maven-coordinate.test.ts | 78 +- packages/maven/tests/maven-pom-parse.test.ts | 340 +++--- packages/maven/tests/maven-samples.ts | 52 +- pages/0-guide.md | 34 +- pnpm-lock.yaml | 161 ++- samples/README.md | 9 +- samples/gradle-platform/README.md | 3 +- samples/modular-guava-maven/README.md | 3 +- samples/modular-guava-repo/README.md | 3 +- samples/modular-guava/README.md | 3 +- samples/modular-proto/README.md | 3 +- socket.yml | 2 + 107 files changed, 7804 insertions(+), 2293 deletions(-) delete mode 100644 packages/gradle/tests/sanity.test.ts delete mode 100644 packages/java/classfile-parser.ts create mode 100644 packages/java/java-classfile.ts rename packages/java/{javahome.ts => java-home.ts} (69%) create mode 100644 packages/java/java-jar.ts create mode 100644 packages/java/java-manifest.ts rename packages/java/{jar-reader.ts => javaclasses/attribute.ts} (83%) create mode 100644 packages/java/javaclasses/constant-pool.ts create mode 100644 packages/java/javaclasses/java-class-types.ts create mode 100644 packages/java/javamodules/jdk-modules.ts create mode 100644 packages/java/javamodules/module-flags.ts delete mode 100644 packages/java/tests/classfile-parser.test.ts create mode 100644 packages/java/tests/compiler/Hello.java create mode 100644 packages/java/tests/compiler/complex/Another.java create mode 100644 packages/java/tests/compiler/complex/Hello.java create mode 100644 packages/java/tests/compiler/complex/Implementation.java create mode 100644 packages/java/tests/compiler/complex/META-INF/services/hello.Service create mode 100644 packages/java/tests/compiler/complex/Service.java create mode 100644 packages/java/tests/compiler/complex/module-info.java create mode 100644 packages/java/tests/compiler/module-info.java create mode 100644 packages/java/tests/compiler/open/Hello.java create mode 100644 packages/java/tests/compiler/open/module-info.java create mode 100644 packages/java/tests/java-classfile-parser.test.ts create mode 100644 packages/java/tests/java-classfile.test.ts create mode 100644 packages/java/tests/java-jar.test.ts create mode 100644 packages/java/tests/java-manifest.test.ts create mode 100644 packages/java/tests/java-toolchain.test.ts delete mode 100644 packages/java/tests/sanity.test.ts create mode 100644 packages/java/tests/testutil.test.ts create mode 100644 packages/java/toolchain/repositories.ts create mode 100644 packages/java/toolchain/tool.ts create mode 100644 socket.yml diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 6cbb9a28..37f28432 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -2,17 +2,14 @@ ## Our Pledge -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making +participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, +disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, +socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards -Examples of behavior that contributes to creating a positive environment -include: +Examples of behavior that contributes to creating a positive environment include: - Using welcoming and inclusive language - Being respectful of differing viewpoints and experiences @@ -22,55 +19,43 @@ include: Examples of unacceptable behavior by participants include: -- The use of sexualized language or imagery and unwelcome sexual attention or - advances +- The use of sexualized language or imagery and unwelcome sexual attention or advances - Trolling, insulting/derogatory comments, and personal or political attacks - Public or private harassment -- Publishing others' private information, such as a physical or electronic - address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take +appropriate and fair corrective action in response to any instances of unacceptable behavior. -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, +issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any +contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope -This Code of Conduct applies within all project spaces, and it also applies when -an individual is representing the project or its community in public spaces. -Examples of representing a project or community include using an official -project e-mail address, posting via an official social media account, or acting -as an appointed representative at an online or offline event. Representation of -a project may be further defined and clarified by project maintainers. +This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the +project or its community in public spaces. Examples of representing a project or community include using an official +project e-mail address, posting via an official social media account, or acting as an appointed representative at an +online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at apps at elide dot cloud. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at +apps at elide dot cloud. All complaints will be reviewed and investigated and will result in a response that is deemed +necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to +the reporter of an incident. Further details of specific enforcement policies may be posted separately. -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent +repercussions as determined by other members of the project's leadership. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at +https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq +For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq diff --git a/.github/GOVERNANCE.md b/.github/GOVERNANCE.md index 2448cd8b..16b921ec 100644 --- a/.github/GOVERNANCE.md +++ b/.github/GOVERNANCE.md @@ -4,8 +4,8 @@ company, which is known formally as _Elide Technologies, Inc._, a registered Delaware Limited Liability Company. Elide is also supported by contributions from independent engineers all over the world. -Elide is a community project, and we welcome new contributors. We are committed to fostering a welcoming environment. -We expect contributors to follow the [Contributor Covenant Code of Conduct][2] when discussing the project in any forum. +Elide is a community project, and we welcome new contributors. We are committed to fostering a welcoming environment. We +expect contributors to follow the [Contributor Covenant Code of Conduct][2] when discussing the project in any forum. ## Project Leadership diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 8b32a075..6de7f690 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -9,9 +9,8 @@ Security issues are addressed promptly, and we continuously enhance project secu ## Supported Versions -The Elide project is still early, but we intend to support the latest release and immediate past release. -Once the project hits a level of stability suitable for a `1.0` version we will update this document and issue an -LTS release. +The Elide project is still early, but we intend to support the latest release and immediate past release. Once the +project hits a level of stability suitable for a `1.0` version we will update this document and issue an LTS release. **Current version support matrix:** @@ -23,9 +22,9 @@ LTS release. ## Reporting a Vulnerability -**We use GitHub issues to track vulnerabilities.** [Click here][9] to report a new issue. -If you need to report a vulnerability privately, please use the email address on our main GitHub organization page -(`apps` at `elide` dot `cloud`). +**We use GitHub issues to track vulnerabilities.** [Click here][9] to report a new issue. If you need to report a +vulnerability privately, please use the email address on our main GitHub organization page (`apps` at `elide` dot +`cloud`). If you need to provide secure information or your report needs to be encrypted, please use our PGP key, as listed on public key servers at the same email address. @@ -42,8 +41,8 @@ Other (older) releases may receive backports on a case-by-case basis. We will publish security advisories for any vulnerabilities that we address. -These advisories will be published on our GitHub organization page and will be linked to from this document; -the main `elide` repository will also have a link to this document. +These advisories will be published on our GitHub organization page and will be linked to from this document; the main +`elide` repository will also have a link to this document. **At this time, no security advisories have been announced.** @@ -60,9 +59,8 @@ Elide employs Gradle for dependency assurance, with `SHA-256` and `PGP` used for ### Attestations and Signing -Elide ships with [SLSA attestations][3] for all modules, and embeds an SBOM with each binary artifact. -Library releases are signed with PGP and published to Maven Central; all releases are additionally registered with -[Sigstore][4]. +Elide ships with [SLSA attestations][3] for all modules, and embeds an SBOM with each binary artifact. Library releases +are signed with PGP and published to Maven Central; all releases are additionally registered with [Sigstore][4]. Container image bases carry SLSA attestations and are registered with Sigstore. diff --git a/.github/codecov.yml b/.github/codecov.yml index aef8827c..53b3c423 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -1,13 +1,13 @@ codecov: require_ci_to_pass: true - bot: "elidebot" + bot: 'elidebot' notify: wait_for_ci: true coverage: precision: 2 round: down - range: "25...40" + range: '25...40' status: project: default: @@ -15,7 +15,7 @@ coverage: patch: off comment: - layout: "reach,diff,flags,files,footer" + layout: 'reach,diff,flags,files,footer' behavior: default require_changes: false @@ -33,13 +33,13 @@ github_checks: annotations: true ignore: - - "jdk" - - "samples" - - "tools/processor" - - "tools/substrate/injekt" - - "tools/substrate/sekret" - - "tools/substrate/interakt" - - "tools/substrate/compiler-util" - - "tools/plugin/gradle-plugin" - - "packages/proto/proto-flatbuffers" - - "packages/graalvm/src/main/kotlin/elide/runtime/feature" + - 'jdk' + - 'samples' + - 'tools/processor' + - 'tools/substrate/injekt' + - 'tools/substrate/sekret' + - 'tools/substrate/interakt' + - 'tools/substrate/compiler-util' + - 'tools/plugin/gradle-plugin' + - 'packages/proto/proto-flatbuffers' + - 'packages/graalvm/src/main/kotlin/elide/runtime/feature' diff --git a/.github/dependency-review-config.yml b/.github/dependency-review-config.yml index 9046b65f..41f61459 100644 --- a/.github/dependency-review-config.yml +++ b/.github/dependency-review-config.yml @@ -13,12 +13,12 @@ license-check: true vulnerability-check: true -fail-on-severity: "high" +fail-on-severity: 'high' allow-ghsas: ## Allow `node-fetch`, because it is unused in actual outputs made by this library. - - "GHSA-r683-j2x4-v87g" - - "GHSA-w7rc-rwvf-8q5r" + - 'GHSA-r683-j2x4-v87g' + - 'GHSA-w7rc-rwvf-8q5r' ## Allow `jszip`, because we do not use it in the browser. - - "GHSA-jg8v-48h5-wgxg" + - 'GHSA-jg8v-48h5-wgxg' diff --git a/.github/pr-badge.yml b/.github/pr-badge.yml index 0df128c7..b1c658cc 100644 --- a/.github/pr-badge.yml +++ b/.github/pr-badge.yml @@ -12,11 +12,11 @@ # ## Draft/ready for review -- label: "Status" - message: "Draft" - color: "gray" - when: "$isDraft" -- label: "Status" - message: "Ready for review" - color: "green" - when: "$isDraft === false" +- label: 'Status' + message: 'Draft' + color: 'gray' + when: '$isDraft' +- label: 'Status' + message: 'Ready for review' + color: 'green' + when: '$isDraft === false' diff --git a/.github/workflows/check.dependency-review.yml b/.github/workflows/check.dependency-review.yml index 75ebdb55..acfa7fc0 100644 --- a/.github/workflows/check.dependency-review.yml +++ b/.github/workflows/check.dependency-review.yml @@ -1,6 +1,6 @@ -name: "Checks: Dependency Review" +name: 'Checks: Dependency Review' -"on": +'on': workflow_call: {} workflow_dispatch: {} @@ -9,16 +9,16 @@ permissions: jobs: dependency-review: - name: "Dependency Review" + name: 'Dependency Review' runs-on: ubuntu-latest steps: - name: Harden Runner uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: egress-policy: audit - - name: "Setup: Checkout" + - name: 'Setup: Checkout' uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 with: persist-credentials: false - - name: "Checks: Dependency Review" + - name: 'Checks: Dependency Review' uses: actions/dependency-review-action@9129d7d40b8c12c1ed0f60400d00c92d437adcce # v4.1.3 diff --git a/.github/workflows/check.gradle-wrapper.yml b/.github/workflows/check.gradle-wrapper.yml index 718b7db6..cd300097 100644 --- a/.github/workflows/check.gradle-wrapper.yml +++ b/.github/workflows/check.gradle-wrapper.yml @@ -1,6 +1,6 @@ -name: "Checks: Gradle Wrapper" +name: 'Checks: Gradle Wrapper' -"on": +'on': workflow_call: {} workflow_dispatch: {} @@ -9,16 +9,16 @@ permissions: jobs: validation: - name: "Check: Gradle Wrappers" + name: 'Check: Gradle Wrappers' runs-on: ubuntu-latest steps: - name: Harden Runner uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: egress-policy: audit - - name: "Setup: Checkout" + - name: 'Setup: Checkout' uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 with: persist-credentials: false - - name: "Check: Gradle Wrappers" + - name: 'Check: Gradle Wrappers' uses: gradle/wrapper-validation-action@699bb18358f12c5b78b37bb0111d3a0e2276e0e2 # v2.1.1 diff --git a/.github/workflows/check.hashes.yml b/.github/workflows/check.hashes.yml index 36fdd810..b8bc4073 100644 --- a/.github/workflows/check.hashes.yml +++ b/.github/workflows/check.hashes.yml @@ -1,6 +1,6 @@ -name: "Checks: Hashes" +name: 'Checks: Hashes' -"on": +'on': workflow_call: {} workflow_dispatch: {} @@ -9,16 +9,16 @@ permissions: jobs: validation: - name: "Check: Hashes" + name: 'Check: Hashes' runs-on: ubuntu-latest steps: - name: Harden Runner uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: egress-policy: audit - - name: "Setup: Checkout" + - name: 'Setup: Checkout' uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 with: persist-credentials: false - - name: "Check: Hashes" + - name: 'Check: Hashes' uses: sgammon/verify-hashes@cac7d57e01915a3fc9bda26373fb85d3f71dea68 # v1.0.0-rc1 diff --git a/.github/workflows/ci.build-test.yml b/.github/workflows/ci.build-test.yml index bef95b0f..d5c0a0cc 100644 --- a/.github/workflows/ci.build-test.yml +++ b/.github/workflows/ci.build-test.yml @@ -1,18 +1,18 @@ -name: "Build & Test" +name: 'Build & Test' -"on": +'on': workflow_call: inputs: tests: - description: "Run all tests" + description: 'Run all tests' type: boolean default: false secrets: CODECOV_TOKEN: - description: "Codecov Token" + description: 'Codecov Token' required: false BUILDBUDDY_APIKEY: - description: "BuildBuddy API Key" + description: 'BuildBuddy API Key' required: false workflow_dispatch: {} @@ -22,41 +22,41 @@ permissions: jobs: build: - name: "Build Repository" + name: 'Build Repository' runs-on: ubuntu-latest steps: - - name: "Setup: Harden Runner" + - name: 'Setup: Harden Runner' uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: egress-policy: audit - - name: "Setup: Checkout" + - name: 'Setup: Checkout' uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 with: persist-credentials: false submodules: true - - name: "Setup: Java 21" + - name: 'Setup: Java 21' uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 with: - java-version: "21" - distribution: "zulu" - - name: "Setup: Node" + java-version: '21' + distribution: 'zulu' + - name: 'Setup: Node' uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: ${{ vars.NODE_VERSION || '21' }} - - name: "Setup: Bun" + - name: 'Setup: Bun' uses: oven-sh/setup-bun@d3603274aca5625baad52ec06108517a089cdd00 # v1.2.0 with: bun-version: latest - - name: "Setup: PNPM" + - name: 'Setup: PNPM' uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0 with: version: ${{ vars.PNPM_VERSION || '8' }} run_install: | - recursive: true args: [--frozen-lockfile, --strict-peer-dependencies] - - name: "Setup: Bazelisk" + - name: 'Setup: Bazelisk' uses: bazelbuild/setup-bazelisk@b39c379c82683a5f25d34f0d062761f62693e0b2 # v3.0.0 - - name: "Setup: Cache" + - name: 'Setup: Cache' uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 with: path: | @@ -65,15 +65,15 @@ jobs: .m2 ~/.cache/bazel key: jpms-attic-v1-${{ runner.os }} - - name: "Setup: BuildBuddy" + - name: 'Setup: BuildBuddy' run: echo "build --remote_header=x-buildbuddy-api-key=$BUILDBUDDY_KEY" >> ./.github/bazel.rc env: BUILDBUDDY_KEY: ${{ secrets.BUILDBUDDY_APIKEY }} - - name: "Setup: Bazel Configuration" + - name: 'Setup: Bazel Configuration' run: cp -fv ./.github/bazel.rc ./tools/bazel.rc - - name: "Build & Test Repository" + - name: 'Build & Test Repository' run: make TESTS=${{ inputs.tests && 'yes' || 'no' }} SIGNING=no JAVADOC=no SNAPSHOT=yes - - name: "Reporting: Code Coverage" + - name: 'Reporting: Code Coverage' uses: codecov/codecov-action@54bcd8715eee62d40e33596ef5e8f0f48dbbccab # v4.1.0 continue-on-error: true with: @@ -81,9 +81,9 @@ jobs: slug: elide-dev/jpms flags: packages verbose: true - - name: "Build: Packages" + - name: 'Build: Packages' run: pnpm run -r pack - - name: "Artifact: Packages" + - name: 'Artifact: Packages' uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 with: name: javamodules-npm-${{ github.sha }} diff --git a/.github/workflows/ci.dependency-graph.yml b/.github/workflows/ci.dependency-graph.yml index 382dd133..e4de90be 100644 --- a/.github/workflows/ci.dependency-graph.yml +++ b/.github/workflows/ci.dependency-graph.yml @@ -1,6 +1,6 @@ -name: "Dependency Graph" +name: 'Dependency Graph' -"on": +'on': workflow_call: {} workflow_dispatch: {} @@ -9,7 +9,7 @@ permissions: jobs: build-graph: - name: "Dependency Graph" + name: 'Dependency Graph' runs-on: ubuntu-latest permissions: contents: write # needed for graph write @@ -20,22 +20,18 @@ jobs: disable-sudo: true egress-policy: block allowed-endpoints: > - api.azul.com:443 - api.github.com:443 - cdn.azul.com:443 - github.com:443 - jpms.pkg.st:443 + api.azul.com:443 api.github.com:443 cdn.azul.com:443 github.com:443 jpms.pkg.st:443 repo.maven.apache.org:443 - - name: "Setup: Checkout" + - name: 'Setup: Checkout' uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 with: persist-credentials: false - - name: "Setup: Java 21" + - name: 'Setup: Java 21' uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 with: - java-version: "21" - distribution: "zulu" - - name: "Build: Maven Dependency Graph" + java-version: '21' + distribution: 'zulu' + - name: 'Build: Maven Dependency Graph' continue-on-error: true uses: advanced-security/maven-dependency-submission-action@bfd2106013da0957cdede0b6c39fb5ca25ae375e # v4.0.2 with: diff --git a/.github/workflows/ci.publish-package.yml b/.github/workflows/ci.publish-package.yml index 0bdb5538..82696fdc 100644 --- a/.github/workflows/ci.publish-package.yml +++ b/.github/workflows/ci.publish-package.yml @@ -1,49 +1,49 @@ -name: "Publish: Package" +name: 'Publish: Package' on: workflow_call: inputs: package: - description: "Package" + description: 'Package' type: string required: true registry: - description: "Registry" + description: 'Registry' type: string - default: "https://registry.npmjs.org" + default: 'https://registry.npmjs.org' dry-run: - description: "Dry Run" + description: 'Dry Run' type: boolean default: false release: - description: "Release to GitHub" + description: 'Release to GitHub' type: boolean default: false tag: - description: "Release: Tag" + description: 'Release: Tag' type: string draft: - description: "Release: Draft" + description: 'Release: Draft' type: boolean prerelease: - description: "Release: Pre-release" + description: 'Release: Pre-release' type: boolean release-generate: - description: "Release: Generate Notes" + description: 'Release: Generate Notes' type: boolean release-latest: - description: "Release: Latest" + description: 'Release: Latest' type: boolean secrets: PUBLISH_TOKEN: - description: "Publishing Token" + description: 'Publishing Token' required: true workflow_dispatch: inputs: package: - description: "Package" + description: 'Package' type: choice required: true options: @@ -52,36 +52,36 @@ on: - gradle - indexer dry-run: - description: "Dry Run" + description: 'Dry Run' type: boolean default: false registry: - description: "Registry" + description: 'Registry' type: string - default: "https://registry.npmjs.org" + default: 'https://registry.npmjs.org' release: - description: "Release to GitHub" + description: 'Release to GitHub' type: boolean default: false tag: - description: "Release Tag" + description: 'Release Tag' type: string draft: - description: "Release: Draft" + description: 'Release: Draft' type: boolean prerelease: - description: "Release: Pre-release" + description: 'Release: Pre-release' type: boolean release-generate: - description: "Release: Generate Notes" + description: 'Release: Generate Notes' type: boolean release-latest: - description: "Release: Latest" + description: 'Release: Latest' type: boolean jobs: build: - name: "Package: Build (${{ inputs.package }})" + name: 'Package: Build (${{ inputs.package }})' runs-on: ubuntu-latest outputs: hashes: ${{ steps.hash.outputs.hashes }} @@ -89,29 +89,29 @@ jobs: contents: read id-token: write steps: - - name: "Setup: Harden Runner" + - name: 'Setup: Harden Runner' uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: egress-policy: audit - - name: "Setup: Checkout" + - name: 'Setup: Checkout' uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 with: persist-credentials: false - - name: "Setup: Node" + - name: 'Setup: Node' uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: ${{ vars.NODE_VERSION || '21' }} - registry-url: "https://registry.npmjs.org" - - name: "Setup: PNPM" + registry-url: 'https://registry.npmjs.org' + - name: 'Setup: PNPM' uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0 with: version: ${{ vars.PNPM_VERSION || '8' }} run_install: | - recursive: true args: [--frozen-lockfile, --strict-peer-dependencies] - - name: "Build: Package (${{ inputs.package }})" + - name: 'Build: Package (${{ inputs.package }})' run: cd packages/${{ inputs.package }} && pnpm run build && pnpm pack - - name: "Build: Provenance Hashes" + - name: 'Build: Provenance Hashes' shell: bash id: hash run: | @@ -132,7 +132,7 @@ jobs: echo "" echo "hashes=$(cat ./packages/${{ inputs.package }}/pkg-hashes-encoded.txt)" >> "$GITHUB_OUTPUT" - - name: "Artifact: Packages" + - name: 'Artifact: Packages' uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 with: name: javamodules-pkg-${{ inputs.package }}-${{ github.sha }} @@ -145,7 +145,7 @@ jobs: packages/${{ inputs.package }}/pkg-hashes-encoded.txt provenance: - name: "SLSA Provenance (${{ inputs.package }})" + name: 'SLSA Provenance (${{ inputs.package }})' needs: [build] uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0 permissions: @@ -153,27 +153,27 @@ jobs: id-token: write contents: write with: - base64-subjects: "${{ needs.build.outputs.hashes }}" + base64-subjects: '${{ needs.build.outputs.hashes }}' upload-assets: ${{ inputs.dry-run != true }} upload-tag-name: ${{ inputs.tag || github.ref }} draft-release: ${{ inputs.draft }} release: - name: "Release to GitHub (${{ inputs.package }})" + name: 'Release to GitHub (${{ inputs.package }})' needs: [build, provenance] runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') || inputs.release steps: - - name: "Setup: Harden Runner" + - name: 'Setup: Harden Runner' uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: egress-policy: audit - - name: "Artifact: Package" + - name: 'Artifact: Package' id: releaseArtifact uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 with: name: javamodules-pkg-${{ inputs.package }}-${{ github.sha }} - - name: "Publish: Release" + - name: 'Publish: Release' uses: softprops/action-gh-release@d99959edae48b5ffffd7b00da66dcdb0a33a52ee # v2.0.2 with: draft: ${{ inputs.draft }} @@ -186,7 +186,7 @@ jobs: javamodules-${{ inputs.package }}-${{ inputs.tag || github.ref }}.tgz publish-npm: - name: "Publish to Registry (${{ inputs.package }})" + name: 'Publish to Registry (${{ inputs.package }})' needs: [build, provenance, release] runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') || inputs.release @@ -195,29 +195,29 @@ jobs: contents: write packages: write steps: - - name: "Setup: Harden Runner" + - name: 'Setup: Harden Runner' uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: egress-policy: audit - - name: "Setup: Checkout" + - name: 'Setup: Checkout' uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 with: persist-credentials: false - - name: "Setup: Node" + - name: 'Setup: Node' uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: ${{ vars.NODE_VERSION || '21' }} - registry-url: "https://registry.npmjs.org" - - name: "Setup: PNPM" + registry-url: 'https://registry.npmjs.org' + - name: 'Setup: PNPM' uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0 with: version: ${{ vars.PNPM_VERSION || '8' }} run_install: | - recursive: true args: [--frozen-lockfile, --strict-peer-dependencies] - - name: "Build: All Packages" + - name: 'Build: All Packages' run: pnpm run build - - name: "Publish to Registry" + - name: 'Publish to Registry' run: cd packages/${{ inputs.package }} && npm publish ${{ inputs.dry-run && '--dry-run' || '' }} --registry=${{ inputs.registry }} env: NODE_AUTH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} diff --git a/.github/workflows/ci.publish-packages.yml b/.github/workflows/ci.publish-packages.yml index ba4a358d..b264bdcd 100644 --- a/.github/workflows/ci.publish-packages.yml +++ b/.github/workflows/ci.publish-packages.yml @@ -1,69 +1,69 @@ -name: "Publish: Packages" +name: 'Publish: Packages' on: workflow_call: inputs: dry-run: - description: "Dry Run" + description: 'Dry Run' type: boolean default: false registry: - description: "Registry" + description: 'Registry' type: string - default: "https://registry.npmjs.org" + default: 'https://registry.npmjs.org' release: - description: "Release to GitHub" + description: 'Release to GitHub' type: boolean default: false tag: - description: "Release: Tag" + description: 'Release: Tag' type: string draft: - description: "Release: Draft" + description: 'Release: Draft' type: boolean prerelease: - description: "Release: Pre-release" + description: 'Release: Pre-release' type: boolean release-generate: - description: "Release: Generate Notes" + description: 'Release: Generate Notes' type: boolean release-latest: - description: "Release: Latest" + description: 'Release: Latest' type: boolean secrets: NPM_PUBLISH_TOKEN: - description: "NPM Publishing Token" + description: 'NPM Publishing Token' required: true workflow_dispatch: inputs: dry-run: - description: "Dry Run" + description: 'Dry Run' type: boolean default: false registry: - description: "Registry" + description: 'Registry' type: string - default: "https://registry.npmjs.org" + default: 'https://registry.npmjs.org' release: - description: "Release to GitHub" + description: 'Release to GitHub' type: boolean default: false tag: - description: "Release: Tag" + description: 'Release: Tag' type: string draft: - description: "Release: Draft" + description: 'Release: Draft' type: boolean prerelease: - description: "Release: Pre-release" + description: 'Release: Pre-release' type: boolean release-generate: - description: "Release: Generate Notes" + description: 'Release: Generate Notes' type: boolean release-latest: - description: "Release: Latest" + description: 'Release: Latest' type: boolean permissions: @@ -71,7 +71,7 @@ permissions: jobs: publish-npm-java: - name: "Publish to NPM" + name: 'Publish to NPM' uses: ./.github/workflows/ci.publish-package.yml secrets: PUBLISH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} @@ -91,7 +91,7 @@ jobs: release-latest: ${{ inputs.release-latest }} publish-npm-gradle: - name: "Publish to NPM" + name: 'Publish to NPM' uses: ./.github/workflows/ci.publish-package.yml secrets: PUBLISH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} @@ -111,7 +111,7 @@ jobs: release-latest: ${{ inputs.release-latest }} publish-npm-maven: - name: "Publish to NPM" + name: 'Publish to NPM' uses: ./.github/workflows/ci.publish-package.yml secrets: PUBLISH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} diff --git a/.github/workflows/deploy.site.yml b/.github/workflows/deploy.site.yml index 39817584..fbe613fc 100644 --- a/.github/workflows/deploy.site.yml +++ b/.github/workflows/deploy.site.yml @@ -10,12 +10,12 @@ permissions: id-token: write concurrency: - group: "pages" + group: 'pages' cancel-in-progress: false jobs: build: - name: "Build Site" + name: 'Build Site' runs-on: ubuntu-latest steps: - name: Harden Runner @@ -24,31 +24,29 @@ jobs: disable-sudo: true egress-policy: block allowed-endpoints: > - api.github.com:443 - github.com:443 - index.rubygems.org:443 - - name: "Setup: Checkout" + api.github.com:443 github.com:443 index.rubygems.org:443 + - name: 'Setup: Checkout' uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 with: persist-credentials: false - - name: "Setup: Ruby" + - name: 'Setup: Ruby' uses: ruby/setup-ruby@d4526a55538b775af234ba4af27118ed6f8f6677 # v1.172.0 with: - ruby-version: "3.1" + ruby-version: '3.1' bundler-cache: true cache-version: 0 - - name: "Setup: Pages" + - name: 'Setup: Pages' id: pages uses: actions/configure-pages@1f0c5cde4bc74cd7e1254d0cb4de8d49e9068c7d # v4.0.0 - - name: "Build: Jekyll" + - name: 'Build: Jekyll' run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" env: JEKYLL_ENV: production - - name: "Artifact: Site" + - name: 'Artifact: Site' uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 deploy: - name: "Deploy Site" + name: 'Deploy Site' environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} @@ -61,9 +59,7 @@ jobs: disable-sudo: true egress-policy: block allowed-endpoints: > - api.github.com:443 - github.com:443 - index.rubygems.org:443 - - name: "Deploy: Pages" + api.github.com:443 github.com:443 index.rubygems.org:443 + - name: 'Deploy: Pages' id: deployment uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 diff --git a/.github/workflows/on.pr.yml b/.github/workflows/on.pr.yml index 0a4bde0c..56e1e09f 100644 --- a/.github/workflows/on.pr.yml +++ b/.github/workflows/on.pr.yml @@ -1,6 +1,6 @@ -name: "PR" +name: 'PR' -"on": +'on': pull_request: branches: - main @@ -10,25 +10,25 @@ permissions: jobs: build-test: - name: "Build & Test" + name: 'Build & Test' uses: ./.github/workflows/ci.build-test.yml secrets: inherit build-dependency-graph: - name: "Build & Test" + name: 'Build & Test' uses: ./.github/workflows/ci.dependency-graph.yml permissions: contents: write # needed for graph write checks-gradle: - name: "Checks" + name: 'Checks' uses: ./.github/workflows/check.gradle-wrapper.yml checks-dependency-review: - name: "Checks" + name: 'Checks' needs: [build-dependency-graph] uses: ./.github/workflows/check.dependency-review.yml checks-hashes: - name: "Checks" + name: 'Checks' uses: ./.github/workflows/check.hashes.yml diff --git a/.github/workflows/on.push.yml b/.github/workflows/on.push.yml index 0b622728..93342780 100644 --- a/.github/workflows/on.push.yml +++ b/.github/workflows/on.push.yml @@ -1,6 +1,6 @@ -name: "Push" +name: 'Push' -"on": +'on': push: branches: - main @@ -10,19 +10,19 @@ permissions: jobs: build-test: - name: "Build & Test" + name: 'Build & Test' uses: ./.github/workflows/ci.build-test.yml secrets: inherit build-dependency-graph: - name: "Build & Test" + name: 'Build & Test' uses: ./.github/workflows/ci.dependency-graph.yml secrets: inherit permissions: contents: write # needed for graph write deploy-site: - name: "Deploy" + name: 'Deploy' uses: ./.github/workflows/deploy.site.yml permissions: contents: read @@ -30,5 +30,5 @@ jobs: id-token: write checks-hashes: - name: "Checks" + name: 'Checks' uses: ./.github/workflows/check.hashes.yml diff --git a/.github/workflows/on.queue.yml b/.github/workflows/on.queue.yml index 96d0ede4..05202407 100644 --- a/.github/workflows/on.queue.yml +++ b/.github/workflows/on.queue.yml @@ -1,6 +1,6 @@ -name: "PR Group" +name: 'PR Group' -"on": +'on': merge_group: {} permissions: @@ -8,15 +8,15 @@ permissions: jobs: build-test: - name: "Build & Test" + name: 'Build & Test' uses: ./.github/workflows/ci.build-test.yml with: tests: false checks-gradle: - name: "Checks" + name: 'Checks' uses: ./.github/workflows/check.gradle-wrapper.yml checks-hashes: - name: "Checks" + name: 'Checks' uses: ./.github/workflows/check.hashes.yml diff --git a/.github/workflows/on.release.yml b/.github/workflows/on.release.yml index 4bd506b2..75295e7d 100644 --- a/.github/workflows/on.release.yml +++ b/.github/workflows/on.release.yml @@ -1,4 +1,4 @@ -name: "Release" +name: 'Release' on: release: @@ -9,12 +9,12 @@ permissions: jobs: build-test: - name: "Build & Test" + name: 'Build & Test' uses: ./.github/workflows/ci.build-test.yml secrets: inherit publish-npm: - name: "Publish" + name: 'Publish' needs: [build-test] uses: ./.github/workflows/ci.publish-packages.yml secrets: inherit @@ -27,7 +27,7 @@ jobs: release: true deploy-site: - name: "Deploy" + name: 'Deploy' needs: [build-test] uses: ./.github/workflows/deploy.site.yml secrets: inherit diff --git a/.prettierignore b/.prettierignore index 42f9b55e..7bf0e3b8 100644 --- a/.prettierignore +++ b/.prettierignore @@ -23,3 +23,5 @@ dist pnpm-lock.yaml bun.lockb + +packages/*/reports/* diff --git a/.prettierrc b/.prettierrc index 0967ef42..88b39ee8 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1 +1,17 @@ -{} +{ + "printWidth": 120, + "tabWidth": 2, + "useTabs": false, + "semi": false, + "singleQuote": true, + "quoteProps": "as-needed", + "jsxSingleQuote": false, + "trailingComma": "none", + "bracketSpacing": true, + "bracketSameLine": true, + "arrowParens": "avoid", + "proseWrap": "always", + "htmlWhitespaceSensitivity": "css", + "endOfLine": "lf", + "plugins": ["prettier-plugin-packagejson"] +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 048436e8..ffadedaa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,7 @@ # Contribution Guidelines -This repository is open to contributions: fixes and new libraries are both welcome. See the guide below for directions to build and test the codebase, and to add a new library or amend or update an existing oen. +This repository is open to contributions: fixes and new libraries are both welcome. See the guide below for directions +to build and test the codebase, and to add a new library or amend or update an existing oen. ## Build / Test @@ -10,7 +11,8 @@ You can build everything and run all tests with: make ``` -The embedded projects generally use `mvnw` and `gradlew`, where possible, so you should only need a recent copy of Java. [Java 21 on OpenJDK](https://jdk.java.net/21/) is suggested. +The embedded projects generally use `mvnw` and `gradlew`, where possible, so you should only need a recent copy of Java. +[Java 21 on OpenJDK](https://jdk.java.net/21/) is suggested. At least one project calls out to `mvn`, so you may need to [install Maven](https://maven.apache.org/install.html). @@ -65,7 +67,8 @@ make setup dev ## Making changes -Generally, when making changes, make sure to add tests and samples, where applicable. New libraries will need build steps. +Generally, when making changes, make sure to add tests and samples, where applicable. New libraries will need build +steps. ### Adding a library @@ -127,7 +130,8 @@ clean: ## Clean all built targets. #### Mapping versions -You will also need to add variables to pin the version for the added library, both in `SNAPSHOT` and non-`SNAPSHOT` forms. For example: +You will also need to add variables to pin the version for the added library, both in `SNAPSHOT` and non-`SNAPSHOT` +forms. For example: ```make ifeq ($(SNAPSHOT),yes) @@ -145,18 +149,22 @@ Make sure this version variable overrides the published version in the embedded #### Adding a POM -At `install` into the local repository, a custom POM can be used. This is useful because the published POM may not map to the right transitive JPMS-enabled versions. +At `install` into the local repository, a custom POM can be used. This is useful because the published POM may not map +to the right transitive JPMS-enabled versions. Add your pom to `tools/poms`, and see examples of use in the Makefile. #### Adding to Git -Check the `git-add` command in the `Makefile`, making sure that your artifacts are expressed. This ensures they will not be ignored when publishing. +Check the `git-add` command in the `Makefile`, making sure that your artifacts are expressed. This ensures they will not +be ignored when publishing. ## Publishing -File a PR following the above instructions. Once reviewed and merged, the resulting repository artifacts will be published via GitHub Pages automatically. +File a PR following the above instructions. Once reviewed and merged, the resulting repository artifacts will be +published via GitHub Pages automatically. ## Thank you! -If you are reading this, **you rock.** Open source is a team sport, thank you for considering contributions to this project! +If you are reading this, **you rock.** Open source is a team sport, thank you for considering contributions to this +project! diff --git a/README.md b/README.md index b3045e4a..2c5485e0 100644 --- a/README.md +++ b/README.md @@ -25,38 +25,60 @@ - [GitHub Repo](https://github.com/javamodules/attic) - [Docs](https://javamodules.dev) -This repository provides sub-module library overrides for popular Java libraries which don't yet provide JPMS support (at least until some PRs are merged!). There is a Maven repository which contains these artifacts, too, so you can safely use them in your projects. +This repository provides sub-module library overrides for popular Java libraries which don't yet provide JPMS support +(at least until some PRs are merged!). There is a Maven repository which contains these artifacts, too, so you can +safely use them in your projects. -> [!TIP] -> **These libraries should be treated like `SNAPSHOT` versions until a release is issued. If you see hash failures with Gradle, make sure to pass `--refresh-dependencies --write-verification-metadata ...`. With Maven, pass `-U`.** +> [!TIP] > **These libraries should be treated like `SNAPSHOT` versions until a release is issued. If you see hash +> failures with Gradle, make sure to pass `--refresh-dependencies --write-verification-metadata ...`. With Maven, pass +> `-U`.** #### Pending PRs -Tracking issue [here][6] provides the best tracker. Once these PRs are merged and changes are released, this repo becomes obsolete. +Tracking issue [here][6] provides the best tracker. Once these PRs are merged and changes are released, this repo +becomes obsolete. ### What's in the box? -- **[`com.google.errorprone`][2]:** **Error Prone Compiler** "is a static analysis tool for Java that catches common programming mistakes at compile time," built by Google. Error Prone's annotations module is JPMS-enabled at the embedded sub-module, and is used by Guava. The [PR enabling JPMS support in Error Prone Annotations][3] has been filed, merged, and released, as [`2.26.1`](https://github.com/google/error-prone/releases/tag/v2.26.1). +- **[`com.google.errorprone`][2]:** **Error Prone Compiler** "is a static analysis tool for Java that catches common + programming mistakes at compile time," built by Google. Error Prone's annotations module is JPMS-enabled at the + embedded sub-module, and is used by Guava. The [PR enabling JPMS support in Error Prone Annotations][3] has been + filed, merged, and released, as [`2.26.1`](https://github.com/google/error-prone/releases/tag/v2.26.1). -- **[`com.google.guava`][11]:** **Google Guava** is Google's core Java commons, used throughout Google's code and the wider JVM ecosystem. Guava is an immensely popular artifact, with tons of fantastic utilities. JPMS support is [in draft][12]. +- **[`com.google.guava`][11]:** **Google Guava** is Google's core Java commons, used throughout Google's code and the + wider JVM ecosystem. Guava is an immensely popular artifact, with tons of fantastic utilities. JPMS support is [in + draft][12]. -- **[`com.google.j2objc`][4]:** **J2ObjC** is a Java to Objective-C cross-compiling layer used by Google to effectively share Java logic on iOS and macOS platforms. J2ObjC itself is very complex and powerful, but here we have just JPMS-enabled the `annotations` module, which is used by Guava. The [PR enabling JPMS support for J2ObjC annotations][5] has been filed, merged, and released as [`3.0.0`](https://github.com/google/j2objc/commit/a883dd3f90d51d5ccad4aa3af8feaaeed6560109). +- **[`com.google.j2objc`][4]:** **J2ObjC** is a Java to Objective-C cross-compiling layer used by Google to effectively + share Java logic on iOS and macOS platforms. J2ObjC itself is very complex and powerful, but here we have just + JPMS-enabled the `annotations` module, which is used by Guava. The [PR enabling JPMS support for J2ObjC + annotations][5] has been filed, merged, and released as + [`3.0.0`](https://github.com/google/j2objc/commit/a883dd3f90d51d5ccad4aa3af8feaaeed6560109). -- **[`com.google.protobuf`][4]:** **Protocol Buffers** (a.k.a., protobuf) are Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data. JPMS support is [in draft][17]. +- **[`com.google.protobuf`][4]:** **Protocol Buffers** (a.k.a., protobuf) are Google's language-neutral, + platform-neutral, extensible mechanism for serializing structured data. JPMS support is [in draft][17]. -- **[`io.leangen.geantyref`][18]:** **Geantyref** is a fork of the excellent GenTyRef library, adding support for working with AnnotatedTypes introduced in Java 8 plus many nifty features. JPMS support has been [proposed][19], but not yet merged or released. +- **[`io.leangen.geantyref`][18]:** **Geantyref** is a fork of the excellent GenTyRef library, adding support for + working with AnnotatedTypes introduced in Java 8 plus many nifty features. JPMS support has been [proposed][19], but + not yet merged or released. -- **[`kotlinx.collections.immutable`][20]:** **KotlinX Immutable Collections** is a library provided as part of the _Kotlin Extensions_ suite, maintained by the JetBrains team. It provides immutable and persistent collection types in Kotlin. JPMS support is in [draft][21], but not yet merged or released. +- **[`kotlinx.collections.immutable`][20]:** **KotlinX Immutable Collections** is a library provided as part of the + _Kotlin Extensions_ suite, maintained by the JetBrains team. It provides immutable and persistent collection types in + Kotlin. JPMS support is in [draft][21], but not yet merged or released. -- **[`org.checkerframework`][0]:** **Checker Framework** is a type-checking framework for Java. The `checker-qual` package is used by Guava, so it is included here transitively. Checker Framework added a JPMS module definition in a [recent PR][1], so this is sub-moduled at `master`. At the time of this writing no release has taken place. +- **[`org.checkerframework`][0]:** **Checker Framework** is a type-checking framework for Java. The `checker-qual` + package is used by Guava, so it is included here transitively. Checker Framework added a JPMS module definition in a + [recent PR][1], so this is sub-moduled at `master`. At the time of this writing no release has taken place. -- **[`org.reactivestreams`][16]:** **Reactive Streams** is a universal JVM API for building reactive software in an implementation-agnostic manner. +- **[`org.reactivestreams`][16]:** **Reactive Streams** is a universal JVM API for building reactive software in an + implementation-agnostic manner. ### How do I use it? Add this domain as a repository within any JVM build tool: [Maven][7], [Gradle][8], [Bazel][9], [sbt][10]. For example: -> [!NOTE] > **Filing issues:** Please file issues for this repo on [`elide-dev/jpms`](https://github.com/elide-dev/jpms/issues). +> [!NOTE] > **Filing issues:** Please file issues for this repo on +> [`elide-dev/jpms`](https://github.com/elide-dev/jpms/issues). #### Maven @@ -116,7 +138,8 @@ repositories { ### Libraries -You should use a JPMS-enabled library version which has no conflict with Maven Central. Reference the table below to pick a library. +You should use a JPMS-enabled library version which has no conflict with Maven Central. Reference the table below to +pick a library. **Libraries marked `Central` have seen releases in Maven Central,** and so are no longer needed through this repository. @@ -156,7 +179,8 @@ Use the modules in your `module-info.java`: ### BOMs & Catalogs -This repository additionally provides [Maven BOM][13], [Gradle Version Catalog][14], and [Gradle Platform][15] artifacts. These simplify and enforce the use of the right library versions. See below for use. +This repository additionally provides [Maven BOM][13], [Gradle Version Catalog][14], and [Gradle Platform][15] +artifacts. These simplify and enforce the use of the right library versions. See below for use. | Type | Coordinate | Version | | --------------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -217,11 +241,13 @@ dependencies { ### Limitations -This repo does not currently publish source or javadoc JARs. It's not that it couldn't, it's just that mounting classifier-equipped JARs in local repositories is annoying. +This repo does not currently publish source or javadoc JARs. It's not that it couldn't, it's just that mounting +classifier-equipped JARs in local repositories is annoying. ### Sample Projects -Sample projects are provided in the [samples](./samples) directory, which show how to hook up the repository and override libraries. +Sample projects are provided in the [samples](./samples) directory, which show how to hook up the repository and +override libraries. ### Coming soon @@ -232,7 +258,9 @@ Future badges ### Licensing -This repo is open source, licensed under [Apache 2.0](./LICENSE.txt). The libraries listed in this repo may have their own licenses; it is up to you to comply with these. These libraries are only published here for the purpose of early testing and development against new code; no warranty is provided of any kind. +This repo is open source, licensed under [Apache 2.0](./LICENSE.txt). The libraries listed in this repo may have their +own licenses; it is up to you to comply with these. These libraries are only published here for the purpose of early +testing and development against new code; no warranty is provided of any kind. [0]: https://github.com/typetools/checker-framework [1]: https://github.com/typetools/checker-framework/pull/6326 diff --git a/_config.yml b/_config.yml index 2492071b..72b718c4 100644 --- a/_config.yml +++ b/_config.yml @@ -20,12 +20,11 @@ title: JPMS Attic description: >- # this means to ignore newlines until "baseurl:" - Provides a Maven repository which can be used with Java build tools like Maven, - Gradle, Bazel, and so on, to build with popular JPMS-compatible libraries before - they have been publicly released. + Provides a Maven repository which can be used with Java build tools like Maven, Gradle, Bazel, and so on, to build + with popular JPMS-compatible libraries before they have been publicly released. -baseurl: "/" -url: "https://jpms.pkg.st" +baseurl: '/' +url: 'https://jpms.pkg.st' github_username: javamodules remote_theme: pages-themes/primer@v0.6.0 repository: elide-dev/jpms diff --git a/about.markdown b/about.markdown index 33808e90..a59b30ee 100644 --- a/about.markdown +++ b/about.markdown @@ -4,14 +4,13 @@ title: About permalink: /about/ --- -This is the base Jekyll theme. You can find out more info about customizing your Jekyll theme, as well as basic Jekyll usage documentation at [jekyllrb.com](https://jekyllrb.com/) +This is the base Jekyll theme. You can find out more info about customizing your Jekyll theme, as well as basic Jekyll +usage documentation at [jekyllrb.com](https://jekyllrb.com/) -You can find the source code for Minima at GitHub: -[jekyll][jekyll-organization] / +You can find the source code for Minima at GitHub: [jekyll][jekyll-organization] / [minima](https://github.com/jekyll/minima) -You can find the source code for Jekyll at GitHub: -[jekyll][jekyll-organization] / +You can find the source code for Jekyll at GitHub: [jekyll][jekyll-organization] / [jekyll](https://github.com/jekyll/jekyll) [jekyll-organization]: https://github.com/jekyll diff --git a/assets/css/style.scss b/assets/css/style.scss index b3ff9edd..aa99dcfe 100644 --- a/assets/css/style.scss +++ b/assets/css/style.scss @@ -1,4 +1,4 @@ --- --- -@import "jekyll-theme-primer"; +@import 'jekyll-theme-primer'; diff --git a/bun.lockb b/bun.lockb index cebd7802336a77c31833d887e9d1b4ba9d1597c8..7d5406bab2e972dc2f38272ec716ecb87456e7b6 100755 GIT binary patch delta 63299 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 x*9;a77T2T$*^_?#)P#{s+>;e+vKr delta 58832 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 diff --git a/package.json b/package.json index 0f10d4a9..a4df297c 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,8 @@ "name": "@javamodules/attic", "version": "1.0.0", "private": true, - "type": "module", "license": "Apache-2.0", + "type": "module", "workspaces": [ "packages/java", "packages/maven", @@ -11,44 +11,26 @@ "packages/indexer" ], "scripts": { + "build": "pnpm run -r build", "check": "pnpm run /:check$/", "clean": "make clean", - "format": "pnpm run fmt:write", "fmt:check": "prettier . --check", "fmt:write": "prettier . --write", + "format": "pnpm run fmt:write", + "hashcheck": "node node_modules/verify-hashes/dist/cli.js check .", "indexer": "cd packages/indexer && pnpm run index", - "build": "pnpm run -r build", - "test:bun": "pnpm run -r test:bun", - "test:node": "pnpm run -r test:node", - "test": "pnpm run test:bun", - "prettier": "prettier", + "lockfiles": "rm -fr node_modules && bun i && rm -fr node_modules && pnpm i", "prepare": "husky", - "hashcheck": "node node_modules/verify-hashes/dist/cli.js check ." - }, - "devDependencies": { - "@changesets/cli": "2.27.1", - "@commitlint/cli": "19.2.0", - "@commitlint/config-conventional": "19.1.0", - "husky": "9.0.11", - "lint-staged": "15.2.2", - "prettier": "3.2.5", - "typescript": "5.4.2", - "verify-hashes": "1.0.0-rc1" + "prettier": "prettier", + "test": "pnpm run test:bun", + "test:bun": "pnpm run -r test:bun", + "test:node": "pnpm run -r test:node" }, "husky": { "hooks": { "pre-commit": "lint-staged" } }, - "pnpm": { - "allowDeprecated": { - "rollup-plugin-inject": "*", - "sourcemap-codec": "*" - } - }, - "lint-staged": { - "*": "pnpm run check" - }, "commitlint": { "extends": [ "@commitlint/config-conventional" @@ -60,5 +42,25 @@ ] } }, - "packageManager": "pnpm@8.15.5+sha256.4b4efa12490e5055d59b9b9fc9438b7d581a6b7af3b5675eb5c5f447cee1a589" + "lint-staged": { + "*": "pnpm run check" + }, + "devDependencies": { + "@changesets/cli": "2.27.1", + "@commitlint/cli": "19.2.0", + "@commitlint/config-conventional": "19.1.0", + "husky": "9.0.11", + "lint-staged": "15.2.2", + "prettier": "3.2.5", + "prettier-plugin-packagejson": "2.4.12", + "typescript": "5.4.2", + "verify-hashes": "1.0.0-rc1" + }, + "packageManager": "pnpm@8.15.5+sha256.4b4efa12490e5055d59b9b9fc9438b7d581a6b7af3b5675eb5c5f447cee1a589", + "pnpm": { + "allowDeprecated": { + "rollup-plugin-inject": "*", + "sourcemap-codec": "*" + } + } } diff --git a/packages/README.md b/packages/README.md index 47ebb311..0ee18c92 100644 --- a/packages/README.md +++ b/packages/README.md @@ -1,6 +1,7 @@ # Packages -Sub-directories in this path define NPM packages which are published under the `@javamodules` namespace. These packages are used as tools for indexing Maven repositories, parsing POMs and Gradle module definitions, and so on. +Sub-directories in this path define NPM packages which are published under the `@javamodules` namespace. These packages +are used as tools for indexing Maven repositories, parsing POMs and Gradle module definitions, and so on. ## Available Utilities @@ -13,7 +14,8 @@ Sub-directories in this path define NPM packages which are published under the ` ## Usage -Check each module's documentation in the project README or on NPM. These utilities are pretty specific and may not be fully documented or tested. Contributions and issues are welcome. +Check each module's documentation in the project README or on NPM. These utilities are pretty specific and may not be +fully documented or tested. Contributions and issues are welcome. [0]: ./gradle [1]: ./indexer diff --git a/packages/gradle/README.md b/packages/gradle/README.md index fd4e76b0..d960261e 100644 --- a/packages/gradle/README.md +++ b/packages/gradle/README.md @@ -1,10 +1,12 @@ # Java Tools: Gradle -This package provides JavaScript logic for working with [Gradle Module][0] definitions. Schemas and types are provided for modules, as well as functions to load and validate them. +This package provides JavaScript logic for working with [Gradle Module][0] definitions. Schemas and types are provided +for modules, as well as functions to load and validate them. ## About this Project -Part of the _[Java Modules](https://javamodules.dev)_ project; licensed as MIT. Contributions and issues are [welcome][1]. +Part of the _[Java Modules](https://javamodules.dev)_ project; licensed as MIT. Contributions and issues are +[welcome][1]. [0]: https://docs.gradle.org/current/userguide/publishing_gradle_module_metadata.html [1]: https://github.com/javamodules/attic diff --git a/packages/gradle/gradle-constants.ts b/packages/gradle/gradle-constants.ts index a497fd3e..d9ed329d 100644 --- a/packages/gradle/gradle-constants.ts +++ b/packages/gradle/gradle-constants.ts @@ -14,4 +14,4 @@ /** * Currently-latest Gradle Module schema version. */ -export const GRADLE_SCHEMA_VERSION = "1.1"; +export const GRADLE_SCHEMA_VERSION = '1.1' diff --git a/packages/gradle/gradle-facade.ts b/packages/gradle/gradle-facade.ts index 65c9fe3a..d5150596 100644 --- a/packages/gradle/gradle-facade.ts +++ b/packages/gradle/gradle-facade.ts @@ -11,9 +11,9 @@ * License for the specific language governing permissions and limitations under the License. */ -import { basename, dirname } from "node:path"; -import { GradleAttribute, GradleModuleSchema, GradleVariantSchema } from "./gradle-schema"; -import { GradleModuleOptions, gradleModule } from "./gradle-util"; +import { basename, dirname } from 'node:path' +import { GradleAttribute, GradleModuleSchema, GradleVariantSchema } from './gradle-schema' +import { GradleModuleOptions, gradleModule } from './gradle-util' /** * Gradle Info @@ -28,7 +28,7 @@ export interface GradleInfo { * @param attr Attribute to obtain a value for * @return Attribute value as a `string`, or `null` */ - attribute(attr: GradleAttribute | string): string | null; + attribute(attr: GradleAttribute | string): string | null } /** @@ -49,7 +49,7 @@ export class GradleVariant implements GradleInfo { * @return Value as a string, or `null` */ attribute(attr: GradleAttribute): string | null { - return this._variant.attributes[attr] || null; + return this._variant.attributes[attr] || null } /** @@ -59,7 +59,7 @@ export class GradleVariant implements GradleInfo { * @returns Wrapped variant data */ static fromData(data: GradleVariantSchema): GradleVariant { - return new GradleVariant(data); + return new GradleVariant(data) } } @@ -82,21 +82,21 @@ export class GradleModule implements GradleInfo { */ private constructor( private readonly _module: GradleModuleSchema, - private readonly _file: string | null, + private readonly _file: string | null ) {} /** * @returns Underlying Gradle Module data */ module(): GradleModuleSchema { - return this._module; + return this._module } /** * @returns File from which this definition was parsed, as applicable */ file(): string | null { - return this._file; + return this._file } /** @@ -106,9 +106,9 @@ export class GradleModule implements GradleInfo { * @return Requested Gradle Variant, or `null` */ variant(name: string): GradleVariant | null { - const variantData = this._module.variants.find((it) => it.name === name); - if (variantData) return GradleVariant.fromData(variantData); - return null; + const variantData = this._module.variants.find(it => it.name === name) + if (variantData) return GradleVariant.fromData(variantData) + return null } /** @@ -118,7 +118,7 @@ export class GradleModule implements GradleInfo { * @return Value as a string, or `null` */ attribute(attr: GradleAttribute | string): string | null { - return this._module.component.attributes[attr] || null; + return this._module.component.attributes[attr] || null } /** @@ -129,7 +129,7 @@ export class GradleModule implements GradleInfo { * @return Wrapped module definition */ static fromData(schema: GradleModuleSchema, file?: string): GradleModule { - return new GradleModule(schema, file || null); + return new GradleModule(schema, file || null) } /** @@ -141,17 +141,17 @@ export class GradleModule implements GradleInfo { * @return Wrapped module definition */ static async fromFile(path: string, optionalName?: string, options?: GradleModuleOptions): Promise { - const parent = !!optionalName ? path : dirname(path); - const filename = !!optionalName ? optionalName : basename(path); + const parent = !!optionalName ? path : dirname(path) + const filename = !!optionalName ? optionalName : basename(path) const mod = await gradleModule( parent, filename, options || { validate: true, - lenient: true, - }, - ); - if (!mod) throw new Error(`Failed to load Gradle module from file: ${parent}/${filename}`); - return this.fromData(mod.module, mod.path); + lenient: true + } + ) + if (!mod) throw new Error(`Failed to load Gradle module from file: ${parent}/${filename}`) + return this.fromData(mod.module, mod.path) } } diff --git a/packages/gradle/gradle-model.ts b/packages/gradle/gradle-model.ts index 1d0dff98..a153860f 100644 --- a/packages/gradle/gradle-model.ts +++ b/packages/gradle/gradle-model.ts @@ -17,13 +17,13 @@ * Enumerates well-known or commonly used Gradle attribute names */ export enum GradleAttribute { - STATUS = "org.gradle.status", - CATEGORY = "org.gradle.category", - USAGE = "org.gradle.usage", - BUNDLING = "org.gradle.dependency.bundling", - DOCS_TYPE = "org.gradle.docstype", - KOTLIN_PLATFORM = "org.jetbrains.kotlin.platform.type", - KOTLIN_NATIVE_TARGET = "org.jetbrains.kotlin.native.target", + STATUS = 'org.gradle.status', + CATEGORY = 'org.gradle.category', + USAGE = 'org.gradle.usage', + BUNDLING = 'org.gradle.dependency.bundling', + DOCS_TYPE = 'org.gradle.docstype', + KOTLIN_PLATFORM = 'org.jetbrains.kotlin.platform.type', + KOTLIN_NATIVE_TARGET = 'org.jetbrains.kotlin.native.target' } /** @@ -32,8 +32,8 @@ export enum GradleAttribute { * Describes the type structure of generic Gradle attributes, which may or may not be known */ export type GradleAttributes = { - [key: GradleAttribute | string]: string; -}; + [key: GradleAttribute | string]: string +} /** * Gradle Version Spec @@ -41,10 +41,10 @@ export type GradleAttributes = { * Describes the version specification info for a given Gradle Module dependency */ export type GradleVersionSpec = { - requires?: string; - prefers?: string; - strictly?: string; -}; + requires?: string + prefers?: string + strictly?: string +} /** * Gradle Dependency @@ -52,10 +52,10 @@ export type GradleVersionSpec = { * Describes a dependency declared for a Gradle Module */ export type GradleDependencyDeclaration = { - group: string; - module: string; - version: GradleVersionSpec; -}; + group: string + module: string + version: GradleVersionSpec +} /** * Gradle Release File @@ -63,14 +63,14 @@ export type GradleDependencyDeclaration = { * Describes a file which is referenced by a variant in a Gradle Module */ export type GradleReleaseFile = { - name: string; - url: string; - size: number; - md5: string; - sha1: string; - sha256?: string; - sha512?: string; -}; + name: string + url: string + size: number + md5: string + sha1: string + sha256?: string + sha512?: string +} /** * Gradle Variant @@ -78,11 +78,11 @@ export type GradleReleaseFile = { * Describes a single variant of a Gradle module */ export type GradleVariant = { - name: string; - attributes: GradleAttributes; - files?: GradleReleaseFile[]; - dependencies?: GradleDependencyDeclaration[]; -}; + name: string + attributes: GradleAttributes + files?: GradleReleaseFile[] + dependencies?: GradleDependencyDeclaration[] +} /** * Gradle Component @@ -90,12 +90,12 @@ export type GradleVariant = { * Describes the module being provided by a Gradle Module definition */ export type GradleComponent = { - url: string; - group: string; - module: string; - version: string; - attributes: GradleAttributes; -}; + url: string + group: string + module: string + version: string + attributes: GradleAttributes +} /** * Gradle Creator Info @@ -103,8 +103,8 @@ export type GradleComponent = { * Describes information about a Gradle build agent that created a Gradle Module */ export type GradleCreatorInfo = { - version: string; -}; + version: string +} /** * Gradle Created-By @@ -112,8 +112,8 @@ export type GradleCreatorInfo = { * Describes the build agent that created the Gradle Module info */ export type GradleCreatedBy = { - gradle: GradleCreatorInfo; -}; + gradle: GradleCreatorInfo +} /** * Gradle Module @@ -121,11 +121,11 @@ export type GradleCreatedBy = { * Describes the raw type structure of a Gradle module info file */ export type GradleModuleType = { - formatVersion: string; - component: GradleComponent; - createdBy: GradleCreatedBy; - variants: GradleVariant[]; -}; + formatVersion: string + component: GradleComponent + createdBy: GradleCreatedBy + variants: GradleVariant[] +} /** * Gradle Module Info @@ -133,6 +133,6 @@ export type GradleModuleType = { * Gathers the path for a Gradle module file with its contents */ export type GradleModuleInfo = { - path: string; - module: GradleModuleType; -}; + path: string + module: GradleModuleType +} diff --git a/packages/gradle/gradle-schema.ts b/packages/gradle/gradle-schema.ts index 04a3b6e0..bc577578 100644 --- a/packages/gradle/gradle-schema.ts +++ b/packages/gradle/gradle-schema.ts @@ -1,5 +1,5 @@ -import { object, array, string, number, InferType, ObjectSchema } from "yup"; -import { GRADLE_SCHEMA_VERSION } from "./gradle-constants"; +import { object, array, string, number, InferType, ObjectSchema } from 'yup' +import { GRADLE_SCHEMA_VERSION } from './gradle-constants' import { GradleAttribute, @@ -10,8 +10,8 @@ import { GradleDependencyDeclaration, GradleModuleType, GradleReleaseFile, - GradleVariant, -} from "./gradle-model"; + GradleVariant +} from './gradle-model' export type { GradleAttribute, @@ -21,75 +21,75 @@ export type { GradleComponent, GradleDependencyDeclaration, GradleModuleType, - GradleReleaseFile, -}; + GradleReleaseFile +} const gradleComponentType = { - url: string().label("URL").required(), - group: string().label("Group").required(), - module: string().label("Module").required(), - version: string().label("Version").required(), - attributes: object().label("Attributes").required(), -}; + url: string().label('URL').required(), + group: string().label('Group').required(), + module: string().label('Module').required(), + version: string().label('Version').required(), + attributes: object().label('Attributes').required() +} -export const gradleComponentSchema: ObjectSchema = object(gradleComponentType); +export const gradleComponentSchema: ObjectSchema = object(gradleComponentType) const gradleCreatedByType = { gradle: object({ - version: string().label("Gradle Version").required(), + version: string().label('Gradle Version').required() }) - .label("Gradle") - .required(), -}; + .label('Gradle') + .required() +} -export const gradleCreatedBySchema: ObjectSchema = object(gradleCreatedByType); +export const gradleCreatedBySchema: ObjectSchema = object(gradleCreatedByType) export const gradleDependencySchema: ObjectSchema = object({ - group: string().label("Group").required(), - module: string().label("Module").required(), + group: string().label('Group').required(), + module: string().label('Module').required(), version: object({ - requires: string().label("Requires").optional(), - prefers: string().label("Prefers").optional(), - strictly: string().label("Strictly").optional(), + requires: string().label('Requires').optional(), + prefers: string().label('Prefers').optional(), + strictly: string().label('Strictly').optional() }) - .label("Version") - .required(), -}); + .label('Version') + .required() +}) export const gradleFileSchema: ObjectSchema = object({ - name: string().label("Name").required(), - url: string().label("URL").required(), - size: number().label("Size").required(), - md5: string().label("MD5").required(), - sha1: string().label("SHA1").required(), - sha256: string().label("SHA256").optional(), - sha512: string().label("SHA512").optional(), -}); + name: string().label('Name').required(), + url: string().label('URL').required(), + size: number().label('Size').required(), + md5: string().label('MD5').required(), + sha1: string().label('SHA1').required(), + sha256: string().label('SHA256').optional(), + sha512: string().label('SHA512').optional() +}) export const gradleVariantSchema: ObjectSchema = object({ - name: string().label("Name").required(), - attributes: object().label("Attributes").required(), + name: string().label('Name').required(), + attributes: object().label('Attributes').required(), dependencies: array(gradleDependencySchema).optional(), - files: array(gradleFileSchema).optional(), -}); + files: array(gradleFileSchema).optional() +}) export const gradleAttributesSchema: ObjectSchema = object({ // @TODO(sgammon): keyof string, valueof string, known attrs? -}); +}) export const gradleModuleSchema: ObjectSchema = object({ // `formatVersion: "1.1"` - formatVersion: string().label("Format Version").matches(new RegExp(GRADLE_SCHEMA_VERSION)).required(), + formatVersion: string().label('Format Version').matches(new RegExp(GRADLE_SCHEMA_VERSION)).required(), // `component: {...}` - component: object(gradleComponentType).label("Component").required(), + component: object(gradleComponentType).label('Component').required(), // `createdBy: {...}` - createdBy: object(gradleCreatedByType).label("Created By").required(), + createdBy: object(gradleCreatedByType).label('Created By').required(), // `variants: [{...}]` - variants: array(gradleVariantSchema).label("Variants").required(), -}); + variants: array(gradleVariantSchema).label('Variants').required() +}) /** * Type inference for a variant schema. diff --git a/packages/gradle/gradle-util.ts b/packages/gradle/gradle-util.ts index b8cbf894..72790292 100644 --- a/packages/gradle/gradle-util.ts +++ b/packages/gradle/gradle-util.ts @@ -11,10 +11,10 @@ * License for the specific language governing permissions and limitations under the License. */ -import { existsSync } from "node:fs"; -import { join, resolve } from "node:path"; -import { readFile } from "node:fs/promises"; -import { GradleModuleType, GradleModuleInfo } from "./gradle-model"; +import { existsSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { readFile } from 'node:fs/promises' +import { GradleModuleType, GradleModuleInfo } from './gradle-model' /** * Read a Gradle Module definition from the provided absolute path @@ -23,12 +23,12 @@ import { GradleModuleType, GradleModuleInfo } from "./gradle-model"; * @returns Promise for a decoded Gradle module */ export async function readGradleModule(absolutePath: string): Promise { - const contents = await readFile(absolutePath, { encoding: "utf8" }); - if (!contents) throw new Error(`Failed to read Gradle module at path '${absolutePath}'`); + const contents = await readFile(absolutePath, { encoding: 'utf8' }) + if (!contents) throw new Error(`Failed to read Gradle module at path '${absolutePath}'`) - const obj = JSON.parse(contents) as GradleModuleType; - if (Object.keys(obj).length === 0) throw new Error("Parsed Gradle Module is an empty object"); - return obj; + const obj = JSON.parse(contents) as GradleModuleType + if (Object.keys(obj).length === 0) throw new Error('Parsed Gradle Module is an empty object') + return obj } /** @@ -36,11 +36,11 @@ export async function readGradleModule(absolutePath: string): Promise { - const moduleName = pomName.replace(".pom", ".module"); - const moduleNameAlt = pomName.replace(".pom", ".json"); - const modulePath = resolve(join(path, moduleName)); - const modulePathAlt = resolve(join(path, moduleNameAlt)); - const moduleExists = existsSync(modulePath); - const moduleExistsAlt = existsSync(modulePathAlt); + const moduleName = pomName.replace('.pom', '.module') + const moduleNameAlt = pomName.replace('.pom', '.json') + const modulePath = resolve(join(path, moduleName)) + const modulePathAlt = resolve(join(path, moduleNameAlt)) + const moduleExists = existsSync(modulePath) + const moduleExistsAlt = existsSync(modulePathAlt) if (moduleExists || moduleExistsAlt) { - const moduleNameResolved = moduleExists ? moduleName : moduleNameAlt; - const moduleTarget = moduleExists ? modulePath : modulePathAlt; + const moduleNameResolved = moduleExists ? moduleName : moduleNameAlt + const moduleTarget = moduleExists ? modulePath : modulePathAlt const mod = { path: moduleTarget, - module: await readGradleModule(join(path, moduleNameResolved)), - }; - if (options?.validate !== false) validateGradleModule(mod, { strict: true, err: true }); - return mod; + module: await readGradleModule(join(path, moduleNameResolved)) + } + if (options?.validate !== false) validateGradleModule(mod, { strict: true, err: true }) + return mod } - if (options?.lenient !== true) throw new Error(`Failed to locate Gradle Module at '${modulePath}'`); + if (options?.lenient !== true) throw new Error(`Failed to locate Gradle Module at '${modulePath}'`) // no module file found for this release - return undefined; + return undefined } diff --git a/packages/gradle/index.mts b/packages/gradle/index.mts index ee8fec98..a24a9c2d 100644 --- a/packages/gradle/index.mts +++ b/packages/gradle/index.mts @@ -11,6 +11,6 @@ * License for the specific language governing permissions and limitations under the License. */ -export type { GradleModuleInfo } from "./gradle-model"; -export * from "./gradle-schema"; -export * from "./gradle-facade"; +export type { GradleModuleInfo } from './gradle-model' +export * from './gradle-schema' +export * from './gradle-facade' diff --git a/packages/gradle/package.json b/packages/gradle/package.json index 55fed430..846694e0 100644 --- a/packages/gradle/package.json +++ b/packages/gradle/package.json @@ -1,45 +1,27 @@ { "name": "@javamodules/gradle", - "version": "1.0.1", - "type": "module", - "main": "dist/index.mjs", + "version": "1.0.2", "description": "Tools for working with Gradle Module metadata from JavaScript.", - "license": "Apache-2.0", - "homepage": "https://github.com/javamodules", - "files": [ - "dist/**", - "!dist/*test*", - "!dist/tests" - ], "keywords": [ "tools", "java", "gradle" ], - "publishConfig": { - "provenance": true, - "access": "public" + "homepage": "https://github.com/javamodules", + "bugs": { + "url": "https://github.com/elide-dev/jpms/issues" }, "repository": { "type": "git", "url": "https://github.com/elide-dev/jpms", "directory": "packages/maven" }, - "bugs": { - "url": "https://github.com/elide-dev/jpms/issues" - }, + "license": "Apache-2.0", "author": { "name": "Sam Gammon", "url": "https://github.com/sgammon" }, - "scripts": { - "test:bun": "bun test", - "test:node": "node --experimental-vm-modules node_modules/jest/bin/jest.js", - "publish:dry": "npm publish --no-git-checks --dry-run", - "publish:live": "npm publish --no-git-checks", - "pack": "npm pack", - "build": "tsc -p ." - }, + "type": "module", "imports": { "#tests": { "bun": "bun:test", @@ -56,28 +38,29 @@ "types": "./dist/index.d.ts" } }, - "devDependencies": { - "@jest/globals": "29.7.0", - "@types/jest": "29.5.12", - "@types/node": "20.11.29", - "jest": "29.7.0", - "jest-junit": "16.0.0", - "semver": "7.6.0", - "ts-jest": "29.1.2", - "typescript": "5.4.2" - }, - "dependencies": { - "yup": "1.4.0" + "main": "dist/index.mjs", + "files": [ + "dist/**", + "!dist/*test*", + "!dist/tests" + ], + "scripts": { + "build": "tsc -p .", + "pack": "npm pack", + "publish:dry": "npm publish --no-git-checks --dry-run", + "publish:live": "npm publish --no-git-checks", + "test:bun": "bun test", + "test:node": "node --experimental-vm-modules node_modules/jest/bin/jest.js" }, "jest": { - "preset": "ts-jest", "collectCoverage": true, - "coverageProvider": "v8", "coverageDirectory": "reports", + "coverageProvider": "v8", "coverageReporters": [ "lcov", "text-summary" ], + "preset": "ts-jest", "reporters": [ "default", "github-actions", @@ -92,5 +75,22 @@ "testMatch": [ "/tests/*.test.ts" ] + }, + "dependencies": { + "yup": "1.4.0" + }, + "devDependencies": { + "@jest/globals": "29.7.0", + "@types/jest": "29.5.12", + "@types/node": "20.11.29", + "jest": "29.7.0", + "jest-junit": "16.0.0", + "semver": "7.6.0", + "ts-jest": "29.1.2", + "typescript": "5.4.2" + }, + "publishConfig": { + "access": "public", + "provenance": true } } diff --git a/packages/gradle/tests/gradle-facade.test.ts b/packages/gradle/tests/gradle-facade.test.ts index 1ef31b74..b1d73ecb 100644 --- a/packages/gradle/tests/gradle-facade.test.ts +++ b/packages/gradle/tests/gradle-facade.test.ts @@ -11,116 +11,116 @@ * License for the specific language governing permissions and limitations under the License. */ -import { expect, test } from "@jest/globals"; -import { join } from "node:path"; -import { GradleModule } from "../gradle-facade"; -import { GradleAttribute, GradleModuleType } from "../gradle-model"; -import { gradleModule } from "../gradle-util"; - -test("gradle module facade instance parse from file", async () => { - const mod = await GradleModule.fromFile(__dirname, "gradle-module-sample.pom"); - expect(mod).not.toBeNull(); - expect(mod.file()).not.toBeNull(); - expect(mod.module()).not.toBeNull(); -}); - -test("gradle module facade instance parse from file (from data)", async () => { - const sampleModule = (await gradleModule(__dirname, "gradle-module-sample.json"))?.module; - expect(sampleModule).toBeDefined(); - const mod = await GradleModule.fromData(sampleModule as GradleModuleType); - expect(mod).not.toBeNull(); - expect(mod.file()).toBeNull(); - expect(mod.module()).not.toBeNull(); -}); - -test("gradle module facade instance parse from file (unknown)", async () => { - let caught = false; +import { expect, test } from '@jest/globals' +import { join } from 'node:path' +import { GradleModule } from '../gradle-facade' +import { GradleAttribute, GradleModuleType } from '../gradle-model' +import { gradleModule } from '../gradle-util' + +test('gradle module facade instance parse from file', async () => { + const mod = await GradleModule.fromFile(__dirname, 'gradle-module-sample.pom') + expect(mod).not.toBeNull() + expect(mod.file()).not.toBeNull() + expect(mod.module()).not.toBeNull() +}) + +test('gradle module facade instance parse from file (from data)', async () => { + const sampleModule = (await gradleModule(__dirname, 'gradle-module-sample.json'))?.module + expect(sampleModule).toBeDefined() + const mod = await GradleModule.fromData(sampleModule as GradleModuleType) + expect(mod).not.toBeNull() + expect(mod.file()).toBeNull() + expect(mod.module()).not.toBeNull() +}) + +test('gradle module facade instance parse from file (unknown)', async () => { + let caught = false try { - await GradleModule.fromFile(__dirname, "gradle-module-sample-unknown.pom"); + await GradleModule.fromFile(__dirname, 'gradle-module-sample-unknown.pom') } catch (err) { - caught = true; + caught = true } - expect(caught).toBeTruthy(); -}); + expect(caught).toBeTruthy() +}) -test("gradle module facade instance parse from file (unknown, full path)", async () => { - let caught = false; +test('gradle module facade instance parse from file (unknown, full path)', async () => { + let caught = false try { - await GradleModule.fromFile(join(__dirname, "gradle-module-sample-unknown.pom"), undefined, { validate: true }); + await GradleModule.fromFile(join(__dirname, 'gradle-module-sample-unknown.pom'), undefined, { validate: true }) } catch (err) { - caught = true; + caught = true } - expect(caught).toBeTruthy(); -}); - -test("gradle module facade instance parse from file with options (full path)", async () => { - const mod = await GradleModule.fromFile(join(__dirname, "gradle-module-sample.pom"), undefined, { validate: true }); - expect(mod).not.toBeNull(); - expect(mod.file()).not.toBeNull(); - expect(mod.module()).not.toBeNull(); -}); - -test("gradle module facade instance parse from file with options (validate)", async () => { - const mod = await GradleModule.fromFile(__dirname, "gradle-module-sample.pom", { validate: true }); - expect(mod).not.toBeNull(); - expect(mod.file()).not.toBeNull(); - expect(mod.module()).not.toBeNull(); -}); - -test("gradle module facade instance parse from file with options (non-validate)", async () => { - const mod = await GradleModule.fromFile(__dirname, "gradle-module-sample.pom", { validate: false }); - expect(mod).not.toBeNull(); - expect(mod.file()).not.toBeNull(); - expect(mod.module()).not.toBeNull(); -}); - -test("gradle module facade instance parse from file with options (lenient)", async () => { - const mod = await GradleModule.fromFile(__dirname, "gradle-module-sample.pom", { lenient: true }); - expect(mod).not.toBeNull(); - expect(mod.file()).not.toBeNull(); - expect(mod.module()).not.toBeNull(); -}); - -test("gradle module facade instance parse from file with options (non-lenient)", async () => { - const mod = await GradleModule.fromFile(__dirname, "gradle-module-sample.pom", { lenient: false }); - expect(mod).not.toBeNull(); - expect(mod.file()).not.toBeNull(); - expect(mod.module()).not.toBeNull(); -}); - -test("gradle module facade resolve component attribute", async () => { - const mod = await GradleModule.fromFile(__dirname, "gradle-module-sample.pom"); - expect(mod).not.toBeNull(); - expect(mod.file()).not.toBeNull(); - expect(mod.module()).not.toBeNull(); - - expect(mod.attribute(GradleAttribute.STATUS)).not.toBeNull(); - expect(mod.attribute(GradleAttribute.STATUS)).toBe("release"); -}); - -test("gradle module facade resolve unknown attribute", async () => { - const mod = await GradleModule.fromFile(__dirname, "gradle-module-sample.pom"); - expect(mod).not.toBeNull(); - expect(mod.file()).not.toBeNull(); - expect(mod.module()).not.toBeNull(); - - expect(mod.attribute("unknown")).toBeNull(); -}); - -test("gradle module facade resolve unknown variant by name", async () => { - const mod = await GradleModule.fromFile(__dirname, "gradle-module-sample.pom"); - expect(mod).not.toBeNull(); - expect(mod.file()).not.toBeNull(); - expect(mod.module()).not.toBeNull(); - expect(mod.variant("unknown")).toBeNull(); -}); - -test("gradle module facade resolve variant by name", async () => { - const mod = await GradleModule.fromFile(__dirname, "gradle-module-sample.pom"); - expect(mod).not.toBeNull(); - expect(mod.file()).not.toBeNull(); - expect(mod.module()).not.toBeNull(); - - const variant = mod.variant("watchosX64ApiElements-published"); - expect(variant).not.toBeNull(); -}); + expect(caught).toBeTruthy() +}) + +test('gradle module facade instance parse from file with options (full path)', async () => { + const mod = await GradleModule.fromFile(join(__dirname, 'gradle-module-sample.pom'), undefined, { validate: true }) + expect(mod).not.toBeNull() + expect(mod.file()).not.toBeNull() + expect(mod.module()).not.toBeNull() +}) + +test('gradle module facade instance parse from file with options (validate)', async () => { + const mod = await GradleModule.fromFile(__dirname, 'gradle-module-sample.pom', { validate: true }) + expect(mod).not.toBeNull() + expect(mod.file()).not.toBeNull() + expect(mod.module()).not.toBeNull() +}) + +test('gradle module facade instance parse from file with options (non-validate)', async () => { + const mod = await GradleModule.fromFile(__dirname, 'gradle-module-sample.pom', { validate: false }) + expect(mod).not.toBeNull() + expect(mod.file()).not.toBeNull() + expect(mod.module()).not.toBeNull() +}) + +test('gradle module facade instance parse from file with options (lenient)', async () => { + const mod = await GradleModule.fromFile(__dirname, 'gradle-module-sample.pom', { lenient: true }) + expect(mod).not.toBeNull() + expect(mod.file()).not.toBeNull() + expect(mod.module()).not.toBeNull() +}) + +test('gradle module facade instance parse from file with options (non-lenient)', async () => { + const mod = await GradleModule.fromFile(__dirname, 'gradle-module-sample.pom', { lenient: false }) + expect(mod).not.toBeNull() + expect(mod.file()).not.toBeNull() + expect(mod.module()).not.toBeNull() +}) + +test('gradle module facade resolve component attribute', async () => { + const mod = await GradleModule.fromFile(__dirname, 'gradle-module-sample.pom') + expect(mod).not.toBeNull() + expect(mod.file()).not.toBeNull() + expect(mod.module()).not.toBeNull() + + expect(mod.attribute(GradleAttribute.STATUS)).not.toBeNull() + expect(mod.attribute(GradleAttribute.STATUS)).toBe('release') +}) + +test('gradle module facade resolve unknown attribute', async () => { + const mod = await GradleModule.fromFile(__dirname, 'gradle-module-sample.pom') + expect(mod).not.toBeNull() + expect(mod.file()).not.toBeNull() + expect(mod.module()).not.toBeNull() + + expect(mod.attribute('unknown')).toBeNull() +}) + +test('gradle module facade resolve unknown variant by name', async () => { + const mod = await GradleModule.fromFile(__dirname, 'gradle-module-sample.pom') + expect(mod).not.toBeNull() + expect(mod.file()).not.toBeNull() + expect(mod.module()).not.toBeNull() + expect(mod.variant('unknown')).toBeNull() +}) + +test('gradle module facade resolve variant by name', async () => { + const mod = await GradleModule.fromFile(__dirname, 'gradle-module-sample.pom') + expect(mod).not.toBeNull() + expect(mod.file()).not.toBeNull() + expect(mod.module()).not.toBeNull() + + const variant = mod.variant('watchosX64ApiElements-published') + expect(variant).not.toBeNull() +}) diff --git a/packages/gradle/tests/gradle-model.test.ts b/packages/gradle/tests/gradle-model.test.ts index 88315b89..35921045 100644 --- a/packages/gradle/tests/gradle-model.test.ts +++ b/packages/gradle/tests/gradle-model.test.ts @@ -11,57 +11,57 @@ * License for the specific language governing permissions and limitations under the License. */ -import { expect, test } from "@jest/globals"; -import { gradleModule } from "../gradle-util"; +import { expect, test } from '@jest/globals' +import { gradleModule } from '../gradle-util' -test("gradle model parse from content (pom)", async () => { - const mod = await gradleModule(__dirname, "gradle-module-sample.pom"); - expect(mod).not.toBeNull(); - expect(mod?.path).not.toBeNull(); -}); +test('gradle model parse from content (pom)', async () => { + const mod = await gradleModule(__dirname, 'gradle-module-sample.pom') + expect(mod).not.toBeNull() + expect(mod?.path).not.toBeNull() +}) -test("gradle model parse from content (module)", async () => { - const mod = await gradleModule(__dirname, "gradle-module-sample.json"); - expect(mod).not.toBeNull(); - expect(mod?.path).not.toBeNull(); -}); +test('gradle model parse from content (module)', async () => { + const mod = await gradleModule(__dirname, 'gradle-module-sample.json') + expect(mod).not.toBeNull() + expect(mod?.path).not.toBeNull() +}) -test("gradle model parse from unknown file (strict pom)", async () => { - let caught = false; +test('gradle model parse from unknown file (strict pom)', async () => { + let caught = false try { - await gradleModule(__dirname, "unknown-file.pom"); + await gradleModule(__dirname, 'unknown-file.pom') } catch (err) { - caught = true; + caught = true } - expect(caught).toBeTruthy(); -}); + expect(caught).toBeTruthy() +}) -test("gradle model parse from unknown file (strict module)", async () => { - let caught = false; +test('gradle model parse from unknown file (strict module)', async () => { + let caught = false try { - await gradleModule(__dirname, "unknown-file.module"); + await gradleModule(__dirname, 'unknown-file.module') } catch (err) { - caught = true; + caught = true } - expect(caught).toBeTruthy(); -}); + expect(caught).toBeTruthy() +}) -test("gradle model parse from file (empty)", async () => { - let caught = false; +test('gradle model parse from file (empty)', async () => { + let caught = false try { - await gradleModule(__dirname, "gradle-module-empty.pom"); + await gradleModule(__dirname, 'gradle-module-empty.pom') } catch (err) { - caught = true; + caught = true } - expect(caught).toBeTruthy(); -}); + expect(caught).toBeTruthy() +}) -test("gradle model parse from file (empty object)", async () => { - let caught = false; +test('gradle model parse from file (empty object)', async () => { + let caught = false try { - await gradleModule(__dirname, "gradle-module-empty-object.pom"); + await gradleModule(__dirname, 'gradle-module-empty-object.pom') } catch (err) { - caught = true; + caught = true } - expect(caught).toBeTruthy(); -}); + expect(caught).toBeTruthy() +}) diff --git a/packages/gradle/tests/gradle-schema.test.ts b/packages/gradle/tests/gradle-schema.test.ts index a8ba774e..7447780b 100644 --- a/packages/gradle/tests/gradle-schema.test.ts +++ b/packages/gradle/tests/gradle-schema.test.ts @@ -11,40 +11,40 @@ * License for the specific language governing permissions and limitations under the License. */ -import { expect, test } from "@jest/globals"; -import { gradleModuleSchema } from "../gradle-schema"; -import { gradleModule } from "../gradle-util"; +import { expect, test } from '@jest/globals' +import { gradleModuleSchema } from '../gradle-schema' +import { gradleModule } from '../gradle-util' async function sampleModule() { - return (await gradleModule(__dirname, "gradle-module-sample.json"))?.module; + return (await gradleModule(__dirname, 'gradle-module-sample.json'))?.module } -test("expects gradle module to load for testing", () => { - expect(sampleModule).not.toBeNull(); -}); +test('expects gradle module to load for testing', () => { + expect(sampleModule).not.toBeNull() +}) -test("gradle module should fail with invalid format version", async () => { - let caught = false; +test('gradle module should fail with invalid format version', async () => { + let caught = false try { await gradleModuleSchema.validate({ ...(await sampleModule()), - formatVersion: "invalid-version", - }); + formatVersion: 'invalid-version' + }) } catch (err) { - caught = true; + caught = true } - expect(caught).toBeTruthy(); -}); + expect(caught).toBeTruthy() +}) -test("gradle module should fail with unexpected format version", async () => { - let caught = false; +test('gradle module should fail with unexpected format version', async () => { + let caught = false try { await gradleModuleSchema.validate({ ...(await sampleModule()), - formatVersion: "1.2", - }); + formatVersion: '1.2' + }) } catch (err) { - caught = true; + caught = true } - expect(caught).toBeTruthy(); -}); + expect(caught).toBeTruthy() +}) diff --git a/packages/gradle/tests/sanity.test.ts b/packages/gradle/tests/sanity.test.ts deleted file mode 100644 index df2141cd..00000000 --- a/packages/gradle/tests/sanity.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright (c) 2024 Elide Technologies, Inc. - * - * Licensed under the MIT license (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://opensource.org/license/mit/ - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under the License. - */ - -import { expect, test } from "@jest/globals"; - -test("2 + 2", () => { - expect(2 + 2).toBe(4); -}); diff --git a/packages/indexer/indexer-model.mts b/packages/indexer/indexer-model.mts index ba91d107..5e0b5ffd 100644 --- a/packages/indexer/indexer-model.mts +++ b/packages/indexer/indexer-model.mts @@ -1,5 +1,5 @@ -import { MavenCoordinate } from "@javamodules/maven"; -import { GradleModuleInfo } from "@javamodules/gradle"; +import { MavenCoordinate } from '@javamodules/maven' +import { GradleModuleInfo } from '@javamodules/gradle' /** * Repository Package @@ -8,12 +8,12 @@ import { GradleModuleInfo } from "@javamodules/gradle"; * content, as needed and applicable */ export type RepositoryPackage = { - maven: MavenCoordinate; - pom: string; - root: string; - gradle?: GradleModuleInfo; - valueOf: any; -}; + maven: MavenCoordinate + pom: string + root: string + gradle?: GradleModuleInfo + valueOf: any +} /** * Indexed Artifact @@ -22,12 +22,12 @@ export type RepositoryPackage = { * Gradle Module, and parsed metadata from the POM and Gradle Module. */ export type IndexedArtifact = { - maven: MavenCoordinate; - pom: string; - name?: string; - description?: string; - gradle?: GradleModuleInfo; -}; + maven: MavenCoordinate + pom: string + name?: string + description?: string + gradle?: GradleModuleInfo +} /** * Repository Artifacts Index @@ -35,7 +35,7 @@ export type IndexedArtifact = { * Describes an index file which maps artifacts to their metadata; includes an index of * all available versions for a given artifact. */ -export type RepositoryArtifactsIndex = {}; +export type RepositoryArtifactsIndex = {} /** * Repository Modules Index @@ -43,7 +43,7 @@ export type RepositoryArtifactsIndex = {}; * Describes an index file which maps indexed artifacts by their Java Module coordinate; * this mapping can produce an offset in the `RepositoryArtifactsIndex`. */ -export type RepositoryModulesIndex = {}; +export type RepositoryModulesIndex = {} /** * Repository Index Bundle @@ -52,9 +52,9 @@ export type RepositoryModulesIndex = {}; * as an intermediate data structure before indexes are written. */ export type RepositoryIndexBundle = { - artifacts: RepositoryArtifactsIndex; - modules: RepositoryModulesIndex; -}; + artifacts: RepositoryArtifactsIndex + modules: RepositoryModulesIndex +} /** * Repository Index File @@ -66,11 +66,11 @@ export type RepositoryIndexBundle = { * Hashes are written to the `.` as peers to the index file. */ export type RepositoryIndexFile = { - name: string; - contents: string; - gzip?: string; - md5: string; - sha1: string; - sha256: string; - sha512: string; -}; + name: string + contents: string + gzip?: string + md5: string + sha1: string + sha256: string + sha512: string +} diff --git a/packages/indexer/indexer.mts b/packages/indexer/indexer.mts index 1dd0c4aa..a18eeae9 100644 --- a/packages/indexer/indexer.mts +++ b/packages/indexer/indexer.mts @@ -11,22 +11,22 @@ * License for the specific language governing permissions and limitations under the License. */ -import { globSync } from "glob"; -import { existsSync } from "node:fs"; -import { readdir, stat, mkdir } from "node:fs/promises"; -import { join, resolve, sep, dirname, basename } from "node:path"; +import { globSync } from 'glob' +import { existsSync } from 'node:fs' +import { readdir, stat, mkdir } from 'node:fs/promises' +import { join, resolve, sep, dirname, basename } from 'node:path' -import { MavenCoordinate, mavenCoordinate } from "@javamodules/maven"; -import { GradleModuleInfo } from "@javamodules/gradle"; -import { gradleModule } from "@javamodules/gradle/util"; +import { MavenCoordinate, mavenCoordinate } from '@javamodules/maven' +import { GradleModuleInfo } from '@javamodules/gradle' +import { gradleModule } from '@javamodules/gradle/util' -import { RepositoryPackage, RepositoryIndexBundle, RepositoryIndexFile } from "./indexer-model.mjs"; +import { RepositoryPackage, RepositoryIndexBundle, RepositoryIndexFile } from './indexer-model.mjs' function repositoryPackage( root: string, maven: MavenCoordinate, pom: string, - gradle?: GradleModuleInfo, + gradle?: GradleModuleInfo ): RepositoryPackage { return { maven, @@ -34,55 +34,64 @@ function repositoryPackage( pom, gradle, valueOf: function () { - return `${pom} (${maven})`; - }, - }; + return `${pom} (${maven})` + } + } } function coordinateForPomPath(prefix: string, path: string) { if (!path.startsWith(prefix)) - throw new Error(`Cannot generate coordinate for path '${path}' which is not under prefix '${prefix}'`); + throw new Error(`Cannot generate coordinate for path '${path}' which is not under prefix '${prefix}'`) // like `/.../jpms/repository/org/reactivestreams/reactive-streams/1.0.5-jpms/reactive-streams-1.0.5-jpms.pom` - const trimmed = path.slice(prefix.length + 1); - const segments = trimmed.split(sep); + const trimmed = path.slice(prefix.length + 1) + const segments = trimmed.split(sep) // like `reactive-streams-1.0.5-jpms.pom` - const pomName = segments[segments.length - 1]; + const pomName = segments[segments.length - 1] // like `org/reactivestreams/reactive-streams/1.0.5-jpms` - const coordinateTarget = trimmed.slice(0, trimmed.length - 1 - pomName.length); - const coordinateSegments = coordinateTarget.split(sep); + const coordinateTarget = trimmed.slice(0, trimmed.length - 1 - pomName.length) + const coordinateSegments = coordinateTarget.split(sep) // like `1.0.5-jpms` - const versionString = coordinateSegments[coordinateSegments.length - 1]; + const versionString = coordinateSegments[coordinateSegments.length - 1] // like `reactive-streams` - const artifactId = coordinateSegments[coordinateSegments.length - 2]; + const artifactId = coordinateSegments[coordinateSegments.length - 2] // like ['org', 'reactivestreams'] - const groupSegments = coordinateSegments.slice(0, coordinateSegments.length - 2); + const groupSegments = coordinateSegments.slice(0, coordinateSegments.length - 2) - return mavenCoordinate(groupSegments.join("."), artifactId, versionString); + return mavenCoordinate(groupSegments.join('.'), artifactId, versionString) } async function buildPackages(prefix: string, path: string) { - const found: RepositoryPackage[] = []; - const pathPoms = globSync(join(path, "**", "*.pom")); + const found: RepositoryPackage[] = [] + const pathPoms = globSync(join(path, '**', '*.pom')) for (const pomPath of pathPoms) { - const coordinate = coordinateForPomPath(prefix, pomPath); - console.log(`- Scanning POM '${coordinate.valueOf()}'`); - found.push(repositoryPackage(path, coordinate, pomPath, await gradleModule(dirname(pomPath), basename(pomPath)))); + const coordinate = coordinateForPomPath(prefix, pomPath) + console.log(`- Scanning POM '${coordinate.valueOf()}'`) + found.push( + repositoryPackage( + path, + coordinate, + pomPath, + await gradleModule(dirname(pomPath), basename(pomPath), { + lenient: true + }) + ) + ) } - return found; + return found } async function buildRootPackage(prefix: string, path: string): Promise { - const filestat = await stat(path); - if (!filestat.isDirectory()) return []; // we are only processing directory roots + const filestat = await stat(path) + if (!filestat.isDirectory()) return [] // we are only processing directory roots - const target = resolve(path); - return await buildPackages(prefix, target); + const target = resolve(path) + return await buildPackages(prefix, target) } // @ts-ignore @@ -90,27 +99,27 @@ function buildIndexes(all_packages: RepositoryPackage[]): RepositoryIndexBundle // @TODO build indexes return { artifacts: [], - modules: [], - }; + modules: [] + } } // @ts-ignore async function prepareContent(indexes: RepositoryIndexBundle): Promise { - return []; + return [] } // @ts-ignore async function writeIndexFile(write: RepositoryIndexFile) {} async function writeIndexes(outpath: string, all_packages: RepositoryPackage[]) { - const resolvedOut = resolve(outpath); + const resolvedOut = resolve(outpath) if (!existsSync(resolvedOut)) { - await mkdir(resolvedOut, { recursive: true }); + await mkdir(resolvedOut, { recursive: true }) } - const indexes = buildIndexes(all_packages); - const writes = await prepareContent(indexes); + const indexes = buildIndexes(all_packages) + const writes = await prepareContent(indexes) for (const write of writes) { - await writeIndexFile(write); + await writeIndexFile(write) } } @@ -121,20 +130,20 @@ async function writeIndexes(outpath: string, all_packages: RepositoryPackage[]) * @param outpath Output path (directory) for generated index files */ export async function buildRepositoryIndexes(path: string, outpath: string) { - const prefix = resolve(path); - console.log(`Scanning repository '${prefix}'...`); - let all_packages: RepositoryPackage[] = []; + const prefix = resolve(path) + console.log(`Scanning repository '${prefix}'...`) + let all_packages: RepositoryPackage[] = [] try { - const files = await readdir(path); + const files = await readdir(path) for (const file of files) { - all_packages = all_packages.concat(await buildRootPackage(prefix, join(path, file))); + all_packages = all_packages.concat(await buildRootPackage(prefix, join(path, file))) } } catch (err) { - console.error(err); + console.error(err) } - writeIndexes(outpath, all_packages); + writeIndexes(outpath, all_packages) } // argument expected is path to repository -await buildRepositoryIndexes(process.argv[2] || join("..", "..", "repository"), "./indexes"); +await buildRepositoryIndexes(process.argv[2] || join('..', '..', 'repository'), './indexes') diff --git a/packages/indexer/package.json b/packages/indexer/package.json index 18f724f4..b9ac5422 100644 --- a/packages/indexer/package.json +++ b/packages/indexer/package.json @@ -1,10 +1,7 @@ { "name": "@javamodules/indexer", - "version": "1.0.1", + "version": "1.0.2", "private": true, - "type": "module", - "license": "Apache-2.0", - "main": "dist/indexer.mjs", "description": "Generates JSON indexes for Maven repositories.", "keywords": [ "java", @@ -12,71 +9,50 @@ "maven-repository", "jvm" ], - "files": [ - "dist/**", - "!dist/*test*", - "!dist/tests" - ], - "imports": { - "#tests": { - "bun": "bun:test", - "default": "@jest/globals" - } - }, - "publishConfig": { - "provenance": true, - "access": "public" + "bugs": { + "url": "https://github.com/elide-dev/jpms/issues" }, "repository": { "type": "git", "url": "https://github.com/elide-dev/jpms", "directory": "packages/indexer" }, - "bugs": { - "url": "https://github.com/elide-dev/jpms/issues" - }, + "license": "Apache-2.0", "author": { "name": "Sam Gammon", "url": "https://github.com/sgammon" }, + "type": "module", + "imports": { + "#tests": { + "bun": "bun:test", + "default": "@jest/globals" + } + }, + "main": "dist/indexer.mjs", + "files": [ + "dist/**", + "!dist/*test*", + "!dist/tests" + ], "scripts": { + "build": "tsc -p .", "index": "bun run build && bun dist/indexer.mjs ../../repository", - "test:bun": "bun test", - "test:node": "node --experimental-vm-modules node_modules/jest/bin/jest.js", + "pack": "npm pack", "publish:dry": "npm publish --no-git-checks --dry-run", "publish:live": "npm publish --no-git-checks", - "pack": "npm pack", - "build": "tsc -p ." - }, - "dependencies": { - "@endo/zip": "1.0.2", - "@javamodules/gradle": "workspace:*", - "@javamodules/java": "workspace:*", - "@javamodules/maven": "workspace:*", - "chalk": "5.3.0", - "commander": "12.0.0", - "glob": "10.3.10", - "inquirer": "9.2.16" - }, - "devDependencies": { - "@jest/globals": "29.7.0", - "@types/jest": "29.5.12", - "@types/node": "20.11.29", - "jest": "29.7.0", - "jest-junit": "16.0.0", - "semver": "7.6.0", - "ts-jest": "29.1.2", - "typescript": "5.4.2" + "test:bun": "bun test", + "test:node": "node --experimental-vm-modules node_modules/jest/bin/jest.js" }, "jest": { - "preset": "ts-jest", "collectCoverage": true, - "coverageProvider": "v8", "coverageDirectory": "reports", + "coverageProvider": "v8", "coverageReporters": [ "lcov", "text-summary" ], + "preset": "ts-jest", "reporters": [ "default", "github-actions", @@ -91,5 +67,33 @@ "testMatch": [ "/tests/*.test.ts" ] + }, + "dependencies": { + "@cloudpss/zstd": "0.2.15", + "@sqlite.org/sqlite-wasm": "3.45.2-build1", + "@javamodules/gradle": "workspace:*", + "@javamodules/java": "workspace:*", + "@javamodules/maven": "workspace:*", + "chalk": "5.3.0", + "commander": "12.0.0", + "fflate": "0.8.2", + "glob": "10.3.10", + "inquirer": "9.2.16", + "snappy-wasm": "0.3.0", + "wasm-gzip": "2.0.3" + }, + "devDependencies": { + "@jest/globals": "29.7.0", + "@types/jest": "29.5.12", + "@types/node": "20.11.29", + "jest": "29.7.0", + "jest-junit": "16.0.0", + "semver": "7.6.0", + "ts-jest": "29.1.2", + "typescript": "5.4.2" + }, + "publishConfig": { + "access": "public", + "provenance": true } } diff --git a/packages/indexer/tests/sanity.test.ts b/packages/indexer/tests/sanity.test.ts index df2141cd..fee5d525 100644 --- a/packages/indexer/tests/sanity.test.ts +++ b/packages/indexer/tests/sanity.test.ts @@ -11,8 +11,8 @@ * License for the specific language governing permissions and limitations under the License. */ -import { expect, test } from "@jest/globals"; +import { expect, test } from '@jest/globals' -test("2 + 2", () => { - expect(2 + 2).toBe(4); -}); +test('2 + 2', () => { + expect(2 + 2).toBe(4) +}) diff --git a/packages/java/README.md b/packages/java/README.md index 3fef1c8c..0e8c2287 100644 --- a/packages/java/README.md +++ b/packages/java/README.md @@ -1,10 +1,12 @@ # Java Tools -This package provides JavaScript logic for working with [Java class files][0], [JARs][1], [Java toolchains][2], and other general Java stuff. +This package provides JavaScript logic for working with [Java class files][0], [JARs][1], [Java toolchains][2], and +other general Java stuff. ## About this Project -Part of the _[Java Modules](https://javamodules.dev)_ project; licensed as MIT. Contributions and issues are [welcome][1]. +Part of the _[Java Modules](https://javamodules.dev)_ project; licensed as MIT. Contributions and issues are +[welcome][1]. [0]: https://en.wikipedia.org/wiki/Java_class_file [1]: https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jarGuide.html diff --git a/packages/java/classfile-parser.ts b/packages/java/classfile-parser.ts deleted file mode 100644 index 81cda4f8..00000000 --- a/packages/java/classfile-parser.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2024 Elide Technologies, Inc. - * - * Licensed under the MIT license (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://opensource.org/license/mit/ - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under the License. - */ - -import { resolve } from "node:path"; -// @ts-ignore -import { existsSync } from "node:fs"; -// @ts-ignore -import { readFile } from "node:fs/promises"; - -import { TextDecoder } from "util"; -import { JavaClassFileReader, ClassFile } from "./javaclasses/java-class-reader"; - -type RawClassData = ClassFile; - -// Read class data from a file, then parse it. -async function readClassfile(path: string): Promise { - const classfilePath = resolve(path); - console.log(`would read from path ${classfilePath}`); - if (!existsSync(classfilePath)) throw new Error(`Class file does not exist: ${classfilePath}`); - return readClassfileData(await readFile(classfilePath)); -} - -// Parse the provided classfile data. -function readClassfileData(data: Buffer): RawClassData { - console.log("would read data" + data); - const classFile = JavaClassFileReader.readData(data); - // @ts-ignore - const textDecoder = new TextDecoder(); - return classFile; -} - -/** - * Java Class File - */ -export class JavaClassFile { - /** - * Primary constructor. - * - * @param _classdata Class data this class-file should wrap for access - */ - private constructor(private readonly _classdata: RawClassData) {} - - /** - * @return Raw parsed class data. - */ - raw(): RawClassData { - return this._classdata; - } - - /** - * Parse Java class information from the provided file - * - * @param file File to parse class info from - * @return Parsed and validated class info - */ - static async fromFile(file: string): Promise { - return new JavaClassFile(await readClassfile(file)); - } - - /** - * Parse Java class information from the provided data - * - * @param data Raw data to parse class info from - * @return Parsed and validated class info - */ - static fromData(data: Buffer): JavaClassFile { - return new JavaClassFile(readClassfileData(data)); - } -} diff --git a/packages/java/index.mts b/packages/java/index.mts index f4e99f03..679450b5 100644 --- a/packages/java/index.mts +++ b/packages/java/index.mts @@ -11,4 +11,4 @@ * License for the specific language governing permissions and limitations under the License. */ -export * from "./java-model.js"; +export * from './java-model.js' diff --git a/packages/java/java-classfile.ts b/packages/java/java-classfile.ts new file mode 100644 index 00000000..12aa941b --- /dev/null +++ b/packages/java/java-classfile.ts @@ -0,0 +1,338 @@ +/* + * Copyright (c) 2024 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under the License. + */ + +import { resolve } from 'node:path' +import { readFile } from 'node:fs/promises' + +import { TextDecoder } from 'util' +import { JavaClassFileReader, ClassFile } from './javaclasses/java-class-reader' +import { + JavaModuleExport, + JavaModuleInfo, + JavaModuleProvides, + JavaModuleRequires, + JavaModuleUses, + JvmTarget, + jvmTargetForLevel +} from './java-model' + +import Modifier from './javaclasses/modifier' +import { AttributeName } from './javaclasses/attribute' +import { ModuleFlags, RequiresFlags } from './javamodules/module-flags' + +import { AttributeInfoBase, ClassInfo, ConstantInfo, ModuleInfoAttributes } from './javaclasses/java-class-types' + +// Raw underlying class data. +type RawClassData = ClassFile + +// Read class data from a file, then parse it. +async function readClassfile(path: string): Promise { + // need to do this to avoid a static import for environments like cloudflare workers + const { existsSync } = require('fs') + const classfilePath = resolve(path) + if (!existsSync(classfilePath)) throw new Error(`Class file does not exist: ${classfilePath}`) + return readClassfileData(await readFile(classfilePath)) +} + +// Parse the provided classfile data. +function readClassfileData(data: Buffer): RawClassData { + const classFile = JavaClassFileReader.readData(data) + // @ts-ignore + const textDecoder = new TextDecoder() + return classFile +} + +// Decode the raw bytes of a constant-pool value into a string. +function decodeConstantToString(constant: ConstantInfo): string { + return String.fromCharCode.apply(null, constant.bytes) +} + +// Decode the internal class name form (`some/class/Name`) into a package-qualified class name (`some.class.Name`). +function internalClassNameToClassName(name: string): string { + return name.replace(/\//g, '.') +} + +// Determine whether a flag is set in a masked flag value. +function flag(value: number, flag: number): boolean { + return (value & flag) !== 0 +} + +/** + * Java Class File + */ +export class JavaClassFile { + /** + * Primary constructor. + * + * @param _classdata Class data this class-file should wrap for access + */ + private constructor(private readonly _classdata: RawClassData) {} + + /** + * @return Raw parsed class data. + */ + raw(): RawClassData { + return this._classdata + } + + /** + * Parse Java class information from the provided file + * + * @param file File to parse class info from + * @return Parsed and validated class info + */ + static async fromFile(file: string): Promise { + return new JavaClassFile(await readClassfile(file)) + } + + /** + * Parse Java class information from the provided data + * + * @param data Raw data to parse class info from + * @return Parsed and validated class info + */ + static fromData(data: Buffer): JavaClassFile { + return new JavaClassFile(readClassfileData(data)) + } + + // Decode a constant from the constant pool. + private constantString(index: number): string { + const constant = this._classdata.constant_pool[index] + if (!constant) throw new Error(`Required constant not found in pool at index ${index}`) + return decodeConstantToString(constant) + } + + // Decode a constant from the constant pool, without failing if it is not found. + private optionalConstantString(index?: number): string | null { + if (!index || index === 0) return null + return this.constantString(index) + } + + // Find a class by matching against the attribute name. + private classAttribute(name: string): X | undefined { + return this._classdata.attributes.find(attr => { + return name === this.constantString(attr.attribute_name_index) + }) + } + + // Decode a module attribute block. + private decodeModuleBlock( + block: any[], + nameAttr: string, + flagsAttr?: string + ): { + [key: string]: any + name: string + flags: number + }[] { + return block.map(info => { + return { + name: this.decodeModuleNameAt(info[nameAttr]), + flags: flagsAttr ? info[flagsAttr] : 0 + } + }) + } + + // Decode a module attribute block. + private decodeModuleBlockWithQualifier( + block: any[], + nameAttr: string, + qualifierAttr: string, + flagsAttr?: string + ): { + [key: string]: any + name: string + flags: number + qualifiers: string[] + }[] { + return block.map(info => { + const qualifierIndexes: number[] = info[qualifierAttr] + const qualifiers: string[] = qualifierIndexes.map(index => { + return this.decodeModuleNameAt(index) + }) + + return { + name: this.decodeModuleNameAt(info[nameAttr]), + flags: flagsAttr ? info[flagsAttr] : 0, + qualifiers + } + }) + } + + // Decode an indirected module name at the specified constant pool index. + private decodeModuleNameAt(index: number): string { + const moduleInfo: ClassInfo = this._classdata.constant_pool[index] + if (!moduleInfo) throw new Error(`Required stanza not found in constant pool at index ${index}`) + const { name_index: nameIndex } = moduleInfo + return this.constantString(nameIndex) + } + + // Decode raw module attributes into a structured module info record. + private decodeModuleInfo(attr: ModuleInfoAttributes): JavaModuleInfo { + const { + module_name_index, + module_flags, + module_version_index, + requires: requiresStatements, + exports: exportStatements, + opens: opensStatements, + uses_index: usesStatements, + provides: providesStatements + } = attr + + // top-level module info + const name = this.decodeModuleNameAt(module_name_index) + const version = this.optionalConstantString(module_version_index) || undefined + const flags = { + open: flag(module_flags, ModuleFlags.OPEN) + } + + // `requires` + const requires: JavaModuleRequires[] = this.decodeModuleBlock( + requiresStatements, + 'requires_index', + 'requires_flags' + ).map(requires => { + return { + module: requires.name, + static: flag(requires.flags, RequiresFlags.STATIC), + transitive: flag(requires.flags, RequiresFlags.TRANSITIVE) + } + }) + + // `exports` + const exports: JavaModuleExport[] = this.decodeModuleBlockWithQualifier( + exportStatements, + 'exports_index', + 'exports_to_index', + 'exports_flags' + ).map(exports => { + return { + package: exports.name, + to: exports.qualifiers.length > 0 ? exports.qualifiers : [] + } + }) + + // `opens` + const opens: JavaModuleExport[] = this.decodeModuleBlockWithQualifier( + opensStatements, + 'opens_index', + 'opens_to_index', + 'opens_flags' + ).map(exports => { + return { + package: exports.name, + to: exports.qualifiers.length > 0 ? exports.qualifiers : [] + } + }) + + // `uses` + const uses: JavaModuleUses[] = usesStatements.map(usesIndex => { + return { + service: internalClassNameToClassName(this.decodeModuleNameAt(usesIndex)) + } + }) + + // `provides` + const provides: JavaModuleProvides[] = this.decodeModuleBlockWithQualifier( + providesStatements, + 'provides_index', + 'provides_with_index' + ).map(provides => { + return { + service: internalClassNameToClassName(provides.name), + with: provides.qualifiers.map(className => internalClassNameToClassName(className)) + } + }) + + return { + name, + version, + requires, + exports, + opens, + uses, + provides, + flags + } + } + + /** + * Determine the bytecode level for this class file + * + * @return JVM target version for this class file + */ + bytecodeTarget(): JvmTarget { + return jvmTargetForLevel(this._classdata.major_version) + } + + /** + * Determine the package-qualified class name for this class file + * + * @return Package-qualified class name + */ + qualifiedName(): string { + // https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4.1 + const classInfo: ClassInfo = this._classdata.constant_pool[this._classdata.this_class] + const { name_index: nameIndex } = classInfo + const className = this.constantString(classInfo.name_index) + if (!className) throw new Error(`Required class name not found in constant pool at declared index ${nameIndex}`) + return internalClassNameToClassName(className) + } + + /** + * Determine the simple, unqualified class name for this class file + * + * @return Simple class name + */ + simpleName(): string { + const simple = this.qualifiedName().split('.').at(-1) + if (!simple) throw new Error(`Failed to determine simple class name from qualified name: ${this.qualifiedName()}`) + return simple + } + + /** + * Determine the package name declared for this class file + * + * @return Package name + */ + packageName(): string { + const qualified = this.qualifiedName() + const parts = qualified.split('.') + parts.pop() + return parts.join('.') + } + + /** + * Determine whether this class-file represents a Java module + * + * @return Whether this class is a compiled module definition + */ + isModule(): boolean { + return flag(this._classdata.access_flags, Modifier.MODULE) + } + + /** + * Obtain information about this class as a Java module + * + * If this class does not represent a compiled module definition, an error is thrown. Please consult `isModule` + * before calling this method. + * + * @return Compiled module info + */ + moduleInfo(): JavaModuleInfo { + const attr = this.classAttribute(AttributeName.MODULE) + if (!attr) throw new Error('This class is not a module') + return this.decodeModuleInfo(attr) + } +} diff --git a/packages/java/javahome.ts b/packages/java/java-home.ts similarity index 69% rename from packages/java/javahome.ts rename to packages/java/java-home.ts index 8a05be12..720aef61 100644 --- a/packages/java/javahome.ts +++ b/packages/java/java-home.ts @@ -11,12 +11,13 @@ * License for the specific language governing permissions and limitations under the License. */ -import cp from "node:child_process"; -import { resolve, join } from "node:path"; -import { env } from "node:process"; -import semver, { SemVer } from "semver"; +import cp from 'node:child_process' +import { resolve, join } from 'node:path' +import { env } from 'node:process' +import semver, { SemVer } from 'semver' -import { JavaCompiler, JavaLauncher } from "./toolchain"; +import { JavaToolchainVendor } from './java-model' +import { JavaCompiler, JavaLauncher, JdkTool } from './toolchain' // Version block regex. // @@ -42,31 +43,22 @@ import { JavaCompiler, JavaLauncher } from "./toolchain"; // prettier-ignore const javaVersionBlockRegex = /openjdk.* \"{0,1}(?[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2})\"{0,1} (?[0-9]{4}-[0-9]{2}-[0-9]{2})\ {0,1}(?LTS){0,1}\nOpenJDK Runtime Environment\ (?GraalVM CE|Oracle GraalVM|Zulu){0,1}.*\ {0,1}[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}(\+(?[0-9]{1,2}(\.[0-9]{0,2}){0,1})){0,1}.*\nOpenJDK\ (?64|32)-Bit\ (?Client|Server)\ VM.*(\,\ (?mixed mode\, sharing|mixed mode),{0,1})\){0,1}/gm -/** - * Java Toolchain Vendor - */ -export enum JavaToolchainVendor { - OPENJDK = "openjdk", - GRAALVM = "graalvm", - AZUL = "azul", -} - /** * Java Version Info */ export type JavaVersionInfo = { - version: string; - lts: boolean; - major: number; - minor: number; - micro: number; - bitness: number; - releaseDate: string; - patch?: string; - vmType?: string; - vmFlags?: string; - vendor?: JavaToolchainVendor | string; -}; + version: string + lts: boolean + major: number + minor: number + micro: number + bitness: number + releaseDate: string + patch?: string + vmType?: string + vmFlags?: string + vendor?: JavaToolchainVendor | string +} enum VersionStringMatchGroup { VERSION = 1, @@ -76,49 +68,49 @@ enum VersionStringMatchGroup { PATCH = 6, BITNESS = 8, VM_TYPE = 9, - VM_FLAGS = 11, + VM_FLAGS = 11 } function buildMatchGroups(regex: RegExp, str: string): any { - const matches: { [key: number]: string } = {}; - let m; + const matches: { [key: number]: string } = {} + let m while ((m = regex.exec(str)) !== null) { // This is necessary to avoid infinite loops with zero-width matches if (m.index === regex.lastIndex) { - regex.lastIndex++; + regex.lastIndex++ } // The result can be accessed through the `m`-variable. m.forEach((match, groupIndex) => { switch (groupIndex) { case VersionStringMatchGroup.VERSION: - matches[VersionStringMatchGroup.VERSION] = match; - break; + matches[VersionStringMatchGroup.VERSION] = match + break case VersionStringMatchGroup.RELEASE_DATE: - matches[VersionStringMatchGroup.RELEASE_DATE] = match; - break; + matches[VersionStringMatchGroup.RELEASE_DATE] = match + break case VersionStringMatchGroup.LTS: - matches[VersionStringMatchGroup.LTS] = match; - break; + matches[VersionStringMatchGroup.LTS] = match + break case VersionStringMatchGroup.VENDOR: - matches[VersionStringMatchGroup.VENDOR] = match; - break; + matches[VersionStringMatchGroup.VENDOR] = match + break case VersionStringMatchGroup.PATCH: - matches[VersionStringMatchGroup.PATCH] = match; - break; + matches[VersionStringMatchGroup.PATCH] = match + break case VersionStringMatchGroup.BITNESS: - matches[VersionStringMatchGroup.BITNESS] = match; - break; + matches[VersionStringMatchGroup.BITNESS] = match + break case VersionStringMatchGroup.VM_TYPE: - matches[VersionStringMatchGroup.VM_TYPE] = match; - break; + matches[VersionStringMatchGroup.VM_TYPE] = match + break case VersionStringMatchGroup.VM_FLAGS: - matches[VersionStringMatchGroup.VM_FLAGS] = match; - break; + matches[VersionStringMatchGroup.VM_FLAGS] = match + break default: - break; + break } - }); + }) } return { version: matches[VersionStringMatchGroup.VERSION], @@ -128,8 +120,8 @@ function buildMatchGroups(regex: RegExp, str: string): any { vendor: matches[VersionStringMatchGroup.VENDOR], bitness: matches[VersionStringMatchGroup.BITNESS], vmType: matches[VersionStringMatchGroup.VM_TYPE], - vmFlags: matches[VersionStringMatchGroup.VM_FLAGS], - }; + vmFlags: matches[VersionStringMatchGroup.VM_FLAGS] + } } /** @@ -139,29 +131,29 @@ function buildMatchGroups(regex: RegExp, str: string): any { * @return Structured information detected from provided output */ export function parseJavaVersionBlock(text: string): JavaVersionInfo { - const groups = buildMatchGroups(javaVersionBlockRegex, text.trim()); - const { version, releaseDate, lts, patch, vendor, bitness, vmType, vmFlags } = groups; + const groups = buildMatchGroups(javaVersionBlockRegex, text.trim()) + const { version, releaseDate, lts, patch, vendor, bitness, vmType, vmFlags } = groups if (!version) throw new Error( - "Failed to parse VM version from text: \n" + text + "\n" + "Groups: " + JSON.stringify(groups, null, " "), - ); - const vmVersion = semver.parse(version); - if (!vmVersion) throw new Error("Failed to parse VM version as semantic version"); + 'Failed to parse VM version from text: \n' + text + '\n' + 'Groups: ' + JSON.stringify(groups, null, ' ') + ) + const vmVersion = semver.parse(version) + if (!vmVersion) throw new Error('Failed to parse VM version as semantic version') return { version: version as string, releaseDate: releaseDate as string, - lts: lts != "", - bitness: parseInt(bitness || "64"), + lts: lts != '', + bitness: parseInt(bitness || '64'), vendor: (vendor as string) || undefined, major: vmVersion.major, minor: vmVersion.minor, micro: vmVersion.patch, patch: patch, vmType: (vmType as string) || undefined, - vmFlags: (vmFlags as string) || undefined, - }; + vmFlags: (vmFlags as string) || undefined + } } /** @@ -170,7 +162,7 @@ export function parseJavaVersionBlock(text: string): JavaVersionInfo { export class JavaToolchain { private constructor( private readonly _path: string, - private readonly _version: JavaVersionInfo, + private readonly _version: JavaVersionInfo ) {} /** @@ -180,7 +172,7 @@ export class JavaToolchain { * @param version Parsed or declared version info. */ static forPath(path: string, version?: JavaVersionInfo): JavaToolchain { - return new JavaToolchain(path, version || this.parseVersion(join(path, "bin", "java"))); + return new JavaToolchain(path, version || this.parseVersion(join(path, 'bin', 'java'))) } /** @@ -189,7 +181,7 @@ export class JavaToolchain { * @param path Absolute path to the `java` binary */ static parseVersion(path: string): JavaVersionInfo { - return parseJavaVersionBlock(cp.execSync(`${path} --version`).toString()); + return parseJavaVersionBlock(cp.execSync(`${path} --version`).toString()) } /** @@ -199,51 +191,58 @@ export class JavaToolchain { * @param version Parsed version or declared version; like `21.0.2`. */ static current(): JavaToolchain { - const home = env["JAVA_HOME"]; - if (!home) throw new Error("No JAVA_HOME set; please set it to detect Java"); - const base = resolve(home); - return this.forPath(base, this.parseVersion(join(base, "bin", "java"))); + const home = env['JAVA_HOME'] + if (!home) throw new Error('No JAVA_HOME set; please set it to detect Java') + const base = resolve(home) + return this.forPath(base, this.parseVersion(join(base, 'bin', 'java'))) } /** * @return Resolved absolute path to this toolchain. */ path(): string { - return this._path; + return this._path } /** * @return Resolved Java version info for this toolchain. */ versionInfo(): JavaVersionInfo { - return this._version; + return this._version } /** * @return Resolved version string for this toolchain. */ version(): string { - return this._version.version; + return this._version.version } /** * @return Parsed semantic version for this toolchain. */ semver(): SemVer { - return semver.parse(this.version()) as SemVer; + return semver.parse(this.version()) as SemVer } /** * @return Java launcher wrapper for this toolchain. */ launcher(): JavaLauncher { - return JavaLauncher.forToolchain(this); + return JavaLauncher.forToolchain(this) } /** * @return Java compiler wrapper for this toolchain. */ compiler(): JavaCompiler { - return JavaCompiler.forToolchain(this); + return JavaCompiler.forToolchain(this) + } + + /** + * @return Java tool wrapper for this toolchain. + */ + tool(name: string): JdkTool { + return JdkTool.forToolchain(this, name) } } diff --git a/packages/java/java-jar.ts b/packages/java/java-jar.ts new file mode 100644 index 00000000..633a5c79 --- /dev/null +++ b/packages/java/java-jar.ts @@ -0,0 +1,1009 @@ +/* + * Copyright (c) 2024 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under the License. + */ + +import { FlateError, Unzip, UnzipFile, UnzipInflate } from 'fflate' +import ByteBuffer from 'bytebuffer' +import JarManifest, { JarManifest as JarManifestApi, JarManifestBaseline, JarManifestBuilder, JarManifestKeyString, manifestPath } from './java-manifest' +import { JavaModuleInfo, QualifiedClassName, SimpleClassName } from './java-model' +import { JavaClassFile } from './java-classfile' + +/** + * Default size to use for chunks of Zip data + */ +const defaultZipChunkSize = 1024 + +/** + * Utility that produces data for a named file from a zip + */ +interface FileContentProducer { + name: string, + read(): Promise +} + +/** + * Describes a streamed/async unzip operation for reading a JAR + */ +type UnzipOperation = { + op: Unzip, + files: Map, + content: Map, +} + +/** + * Chunk of zip data consumed as an async iterable + */ +type ZipChunk = { + chunk: Uint8Array, + final?: boolean, +} + +/** + * Limited metadata kept for all files. + */ +export type ZipFileMetadata = Omit, 'ondata'>, 'terminate'> + +/** + * JAR Compression + * + * Describes compression modes which can be encountered while processing JAR files. + */ +export enum JarCompression { + /** No compression is applied. */ + IDENTITY = 0, + + /** Deflate compression is applied */ + DEFLATE = 1, +} + +/** + * JAR Entry Type + * + * Enumeration which describes the types of entries which may be present within a JAR. + */ +export enum JarEntryType { + /** + * JAR manifest file + */ + MANIFEST = 'manifest', + + /** + * Generic JAR resource file + */ + RESOURCE = 'resource', + + /** + * Compiled class file + */ + CLASS = 'class', + + /** + * Service mapping file + */ + SERVICE = 'service', + + /** + * Compiled module definition + */ + MODULE = 'module' +} + +/** + * JAR Entry + * + * Represents a single entry in a JAR file; specialized by child interfaces for each type of object which can be + * encountered in a JAR. + */ +export interface JarEntry { + /** + * Relative path to the entry within the JAR. + */ + path: string; +} + +/** + * JAR Resource + * + * Describes a generic resource file found within a JAR. + */ +export interface JarResource extends JarEntry { + /** + * Size of the resource as a count of bytes. + */ + size: number; + + /** + * Whether the entry is compressed. + */ + compression?: JarCompression; + + /** + * MIME type of the file, if known. + */ + mime?: string; + + /** + * Deferred file content producer + */ + data: FileContentProducer; +} + +/** + * JAR Manifest + * + * Describes a JAR manifest located within a JAR. + */ +export interface JarManifestEntry extends JarEntry { + /** + * Parsed manifest attributes. + */ + manifest: JarManifest; +} + +/** + * JAR Service Entry + * + * Describes a mapped service and implementation set within the JAR; these are spawned from/serialize to files within + * the `META-INF/services` directory. + */ +export interface JarServiceEntry extends JarEntry { + /** + * Name of the service mapped by this entry + */ + service: QualifiedClassName; + + /** + * Implementations mapped for this service entry + */ + impls: QualifiedClassName[]; +} + +/** + * JAR Class + * + * Describes a generic compiled Java class file found within a JAR. + */ +export interface JarClassEntry extends JarEntry { + /** + * Qualified name of the class. + */ + qualifiedName: QualifiedClassName; + + /** + * Simple name of the class. + */ + simpleName: SimpleClassName; + + /** + * Interpreted/loaded information about the class. + */ + classfile: Promise; +} + +/** + * JAR Module + * + * Specializes the JAR class concept for compiled module descriptor classes. + */ +export interface JarModuleEntry extends JarClassEntry { + /** + * Parsed module information. + */ + moduleInfo: Promise; +} + +/** + * Read the provided zip data chunk-by-chunk in-memory, grabbing files as we go. + * + * If no file filter is provided, all files are mounted. Files are consumed lazily; for each eligible file, a consumer + * function is mounted on the resulting content map. + * + * @param chunkConsumer Consumer of zip chunks + * @param files Files we are interested in + * @paaram predicate Predicate for file inclusion + * @returns Promise for an unzip operation result + */ +async function readZipInMemory( + chunkConsumer: AsyncGenerator, + predicate: JarPredicate, +): Promise { + const unzipper = new Unzip(); + unzipper.register(UnzipInflate); + const readFiles = new Set(); + const allFiles: Map = new Map(); + const content: Map = new Map(); + + const acceptFile = (file: UnzipFile) => { + const buf = new ByteBuffer() + file.ondata = (err: FlateError | null, data: Uint8Array, final: boolean) => { + if (err) { + // cancel file on error + } else { + // gather bytes + buf.append(data) + if (final) { + readFiles.add(file.name); + content.set(file.name, { + name: file.name, + read: async () => buf, + }); + } + } + }; + + // start the stream + file.start(); + } + + unzipper.onfile = file => { + // always keep the file metadata + allFiles.set(file.name, { + name: file.name, + size: file.size, + originalSize: file.originalSize, + compression: file.compression, + }) + + // if the file matches any predicate, include it + if (predicate(file)) acceptFile(file) + }; + + // consume zip data in chunks, passing to unzipper as we go + let gotChunks = false + for await (const chunk of chunkConsumer) { + gotChunks = true + unzipper.push(chunk.chunk, chunk.final === true); + } + if (!gotChunks) throw new Error('No zip chunks provided') + + return { + op: unzipper, + files: allFiles, + content, + } +} + +/** + * Read a JAR file from the provided data buffer + * + * @param data Data buffer to stream from + * @param predicate Filters for eligible JAR contents + * @returns Unzip operation + */ +export async function readZipFromData( + data: Buffer, + predicate: JarPredicate, + chunkSize: number = defaultZipChunkSize, +): Promise { + if (typeof data.length !== 'number' || data.length < 1) { + console.error('invalid buffer', data) + throw new Error( + `Invalid data buffer provided: empty or non-numeric (buffer type: ${typeof data}, length: ${data.length})` + ) + } + + // slice buffer into ~1024 byte sections + const chunks = Math.ceil(data.length / chunkSize) + let offset = 0 + const chunker = async function*() { + for (let i = 0; i < chunks; i++) { + const chunk = data.subarray(offset, offset + chunkSize) + offset += chunkSize + yield { chunk, final: i === chunks - 1 } + } + } + + return readZipInMemory(chunker(), predicate) +} + +/** + * Identify the type of entry under examination within a JAR. + * + * @param path Path to the entry in question + * @return Type of entry + */ +export function identifyEntry(path: string): JarEntryType { + const match = [ + () => path === manifestPath ? JarEntryType.MANIFEST : null, + () => path.endsWith('module-info.class') ? JarEntryType.MODULE : null, + () => path.endsWith('.class') ? JarEntryType.CLASS : null, + () => path.startsWith('META-INF/services/') ? JarEntryType.SERVICE : null, + ] + for (const matcher of match) { + const result = matcher() + if (result) return result + } + return JarEntryType.RESOURCE +} + +/** + * Inflate a JAR entry from raw data. + * + * @param type Type of JAR entry being inflated + * @param path Path to the entry in question + * @param content Raw content for the entry + * @return Promise for an inflated JAR entry + */ +export async function inflateEntry( + type: JarEntryType, + path: string, + content: FileContentProducer, +): Promise { + return new Promise(async (accept, reject) => { + switch (type) { + case JarEntryType.RESOURCE: + accept({ path } as JarResource) + break + + case JarEntryType.MANIFEST: + const manifest = await JarManifest.fromData((await content.read()).buffer) + accept({ path, manifest } as JarManifestEntry) + break + + case JarEntryType.SERVICE: + const servicefile = await content.read() + const service = servicefile.toString('utf-8').trim() + const impls = service.split('\n').map(line => line.trim()).filter(line => line.length > 0) + accept(jarService(path, service, ...impls)) + break + + case JarEntryType.MODULE: + const modulefile: Promise = new Promise(async (innerAccept, innerReject) => { + try { + innerAccept(JavaClassFile.fromData((await content.read()).buffer)) + } catch (err) { + innerReject(err) + } + }) + const moduleinfo: Promise = new Promise(async (innerAccept, innerReject) => { + try { + const file = await modulefile + innerAccept(file.moduleInfo()) + } catch (err) { + innerReject(err) + } + }) + accept({ + path, + classfile: modulefile, + moduleInfo: moduleinfo, + qualifiedName: jarPathToQualifiedClass(path), + simpleName: qualifiedNameToSimpleName(jarPathToQualifiedClass(path)), + } as JarModuleEntry) + break + + case JarEntryType.CLASS: + const classfile = new Promise(async (innerAccept, innerReject) => { + try { + innerAccept(JavaClassFile.fromData((await content.read()).buffer)) + } catch (err) { + innerReject(err) + } + }) + accept({ + classfile, + qualifiedName: jarPathToQualifiedClass(path), + simpleName: qualifiedNameToSimpleName(jarPathToQualifiedClass(path)), + } as JarClassEntry) + break + + default: + reject(new Error('not yet implemented: ' + type)) + break + } + }) +} + +/** + * Low-level function to inflate raw JAR information from a Zip file. + * + * @param data Data buffer to stream from + * @returns Promise for an interpreted manifest (if any) and an async iterable of JAR entries + */ +export async function inflateFromZip(op: Promise): Promise<{ + manifest: JarManifestApi | null, + entries: Map, +}> { + // wait for the unzip operation + const unzip = (await op) + + // try to resolve manifest content + const manifestContent = unzip.content.get(manifestPath) + let foundManifest: Promise + if (manifestContent) { + foundManifest = JarManifest.fromData((await manifestContent.read()).buffer) + } else { + foundManifest = Promise.resolve(null) + } + + // convert all entries into jar entries + const entries: Map = new Map() + for (const [path, content] of unzip.content) { + if (path.endsWith('/')) continue // skip directories + const entryType = identifyEntry(path) + if (entries.has(path)) throw new Error(`duplicate path in jar: ${path}`) + entries.set(path, { + path, + type: entryType, + entry: inflateEntry(entryType, path, content), + }) + } + return { + manifest: await foundManifest, + entries, + } +} + +/** + * Convert a qualified class name to a path within a JAR file. + * + * @param name Qualified class name to build a JAR path for + * @returns JAR path for the class (expected) + */ +export function qualifiedClassToJarPath(name: QualifiedClassName): string { + return name.replace(/\./g, '/') + '.class' +} + +/** + * Convert a path within a JAR file (for a class) to a qualified class name. + * + * @param path Path to the class file within the JAR + * @return Qualified class name + */ +export function jarPathToQualifiedClass(path: string): QualifiedClassName { + return path.replace(/\.class$/, '').replace(/\//g, '.') +} + +/** + * Convert a qualified class name to a simple class name. + * + * @param name Qualified class name to convert + * @returns Simple name for the class + */ +export function qualifiedNameToSimpleName(name: QualifiedClassName): SimpleClassName { + return name.split('.').pop() as SimpleClassName +} + +/** + * JAR Predicate + * + * Predicate filter function for JAR files; returns true if the file should be included in the JAR reader operation. + */ +export type JarPredicate = ((name: ZipFileMetadata) => boolean) & { + /** Invert the match: if `true` is returned, the file is not included. */ + invert?: boolean, +} + +/** + * Base JAR Operation Options + * + * Options which are mixed in to all JAR operation options. + */ +export type BaseJarOperationOptions = { + /** Predicate matchers for JAR contents; if any match, the file is included. */ + predicate: JarPredicate[], +} + +/** + * JAR Reader Options + * + * Specifies options that govern the JAR reader process, including file filters and other settings which avoid extra + * work when decompressing and reading JARs. + */ +export type JarReaderOptions = BaseJarOperationOptions & { + /** Read all file metadata in the JAR eagerly. */ + eager: boolean, +} + +/** + * JAR Entry Iterable Options + * + * Specifies options for generating an efficient iterable over JAR entries. + */ +export type JarEntryIterableOptions = BaseJarOperationOptions & {} + +/** + * Default options to apply when iterating over JAR entries. + */ +const defaultEntryIterableOptions = { + predicate: [], +}; + +type JarPredicateFactory = () => JarPredicate & { + invert: () => JarPredicate, +} + +function predicateFactory(factory: () => JarPredicate): JarPredicateFactory { + // @ts-expect-error + factory.invert = () => () => { + const pred = factory() + return (file: UnzipFile) => !pred(file) + } + return factory as JarPredicateFactory +} + +/** + * JAR Matcher + * + * Provides a set of pre-defined matcher factories for JAR file predicates. + */ +export const jarMatcher: { [key: string]: JarPredicateFactory } = { + /** Match all entries. */ + all: predicateFactory(() => () => true), + + /** Match no entries. */ + none: predicateFactory(() => () => false), + + /** Match classes. */ + classes: predicateFactory(() => (file: ZipFileMetadata) => file.name.endsWith('.class')), + + /** Match resources. */ + resources: predicateFactory(() => (file: ZipFileMetadata) => !file.name.endsWith('.class')), + + /** Match services. */ + services: predicateFactory(() => (file: ZipFileMetadata) => file.name.startsWith('META-INF/services/')), + + /** Match the primary manifest. */ + manifest: predicateFactory(() => (file: ZipFileMetadata) => file.name === 'META-INF/MANIFEST.MF'), + + /** Match all manifests in the JAR. */ + allManifests: predicateFactory(() => (file: ZipFileMetadata) => jarMatcher.manifest()(file) || file.name.startsWith('META-INF/') && file.name.endsWith('.MF')), + + /** Match the primary module info. */ + moduleInfo: predicateFactory(() => (file: ZipFileMetadata) => file.name === 'module-info.class'), + + /** Match all module info declarations in the JAR. */ + allModuleInfos: predicateFactory(() => (file: ZipFileMetadata) => file.name.endsWith('module-info.class')), +} + +// Default reader options. +const defaultReaderOptions: JarReaderOptions = { + eager: false, + predicate: [], +} + +// Matchers which always apply on top of developer-provided predicates. +const unconditionalPredicates: JarPredicate[] = [ + jarMatcher.allManifests(), + jarMatcher.allModuleInfos(), +] + +// Merge defaults to produce final reader options. +function readerOptions(options?: JarReaderOptions): JarReaderOptions { + return { + ...defaultReaderOptions, + ...options, + predicate: [ + ...unconditionalPredicates, + ...options?.predicate || [], + ] + } +} + +/** + * Build a compound predicate for the provided options. + * + * @param options Options for reading a JAR + * @returns Default predicates + */ +export function buildPredicate(options: JarReaderOptions): JarPredicate { + return (file: ZipFileMetadata) => { + for (const pred of options.predicate) { + if (pred(file) === pred.invert) return false + } + return true + } +} + +/** + * JAR Builder + * + * Efficient and lazily-packed/encoded representation of a Java Archive (JAR) file builder, which emits an immutable + * `JarFile` instance when the build operation is complete; such instances can be written to disk to produce a + * compliant JAR file. + * + * The JAR builder works the same way as the JAR file class, but in reverse: it gathers `JarEntry` instances of + * different types, buffering them until the JAR is told to flush; at that point, the JAR builder begins serializing + * and compressing contents as needed, ultimately producing a `JarFile` instance. + */ +export class JarBuilder { + // Name to carry with the JAR. + private _name: string | null = null + + // Entries to be serialized. + private readonly _entries: JarEntry[] = [] + + // Manifest to be serialized. + private readonly _manifest: JarManifestBuilder = JarManifest.builder() + + /** + * Access the name set for the JAR in this builder. + * + * @return Name or `null` + */ + public get name(): string | null { + return this._name + } + + /** + * Access the underlying JAR manifest builder. + * + * @return Manifest builder + */ + public get manifest(): JarManifestBuilder { + return this._manifest + } + + /** + * Add an entry to the JAR. + * + * @param entry Entry to add + */ + public add(entry: JarEntry): this { + this._entries.push(entry) + return this + } + + /** + * Build this builder into an immutable `JarFile` instance. + * + * @return JAR file instance. + */ + public build(): JarFile { + throw new Error('not yet implemented') + } +} + +/** + * Deferred JAR Entry + * + * JAR entry which defers decoding of its contents. + */ +type DeferredJarEntry = { + path: string, + type: JarEntryType, + entry: Promise +} + +/** + * JAR File + * + * Efficient and lazily-parsed representation of a Java Archive (JAR) file; JARs are structured as Zip files, with + * certain conventions that apply for Java class files and resources, and declaring JAR metadata. + * + * JARs typically have a "JAR manifest" file, which is a metadata file that describes the contents of the JAR, and + * lives at the path `META-INF/MANIFEST.MF` within the JAR. + * + * Additional structure may apply: + * + * - Services can be declared within the `META-INF/services` directory; each file is named after a service interface, + * the full qualified path of the file is the name of the service provider. The file contains qualified class names + * for implementations of the service provided by the JAR. + * + * - "Classes" are defined as compiled Java classes, with the `.class` file extension. Classes can be situated anywhere + * within the JAR root; they are structured in a nested hierarchical directory structure that mirrors the package + * declaration of the compiled class. + * + * - "Resources" are defined as non-class files of any sort; resources can technically live anywhere within the JAR, + * but most of the time they live under the `META-INF` directory somewhere other than `services`. + * + * - "Executable JARs" are JARs which declare a main class in their manifest, via the `Main-Class` attribute. Such JARs + * can be run directly by the `java -jar` command. + * + * - "Multi-release JARs" ("MRJARs") can contain compiled classes for multiple Java versions as well as classes held at + * the root of the JAR. Classes are loaded at the highest supported bytecode level by the runtime. + * + * - "Modular JARs" are JAR files which include a `module-info.class` definition, either at the root of the JAR, or at + * the root of an MRJAR versioned class directory (for example, `META-INF/versions/9/module-info.class`). + * + * - "Multi-modular JARs" ("MMJARs") follow all of the above rules, but can contain multiple `module-info.class` files, + * with progressively upgrading versions, located in MRJAR class roots (`META-INF/versions/21/module-info.class`). + * + * This class provides a high-level interface for reading JAR data and parsing a JAR's manifest and module information. + * Archives are read and de-compressed lazily, and class files are parsed on-demand to satisfy calls. + * + * @see fromData To create a JAR file from a data buffer + * @see fromFile To create a JAR file from a file path + */ +export class JarFile implements JarManifestBaseline { + /** + * Primary constructor. + * + * @param _options Reader options + * @param _zip Handle to a streamed/lazily decompressed JAR zip file + * @param _entries Entries in the JAR + * @param _manifest Optional manifest to include in the JAR + */ + private constructor( + private readonly _options: JarReaderOptions, + private readonly _entries: Map, + private readonly _manifest: JarManifestApi | null) {} + + // Walk the contents of the managed JAR file. + private async *contents(): AsyncIterable { + for (const entry of this._entries.values()) { + yield await entry.entry + } + } + + // Decide whether a JAR entry is eligible for consideration. + private eligible(options: JarEntryIterableOptions, file: JarEntry): boolean { + return (options.predicate.length === 0 || options.predicate.some(pred => pred({ + name: file.path, + compression: JarCompression.IDENTITY, // @TODO + }))) + } + + /** + * Reader options + */ + public get options(): JarReaderOptions { + return this._options; + } + + /** + * Interpreted JAR manifest + * + * @return Promise for a manifest + */ + public get manifest(): JarManifestApi | null { + return this._manifest || null + } + + /** + * Whether this JAR is a `Multi-Release` JAR + * + * @return Promise for `Multi-Release` status + */ + public get multiRelease(): boolean { + return this.manifest?.multiRelease || false + } + + /** + * Whether this JAR is modular + * + * @return Promise for modular status + */ + public get modular(): Promise { + throw new Error('not yet implemented') + } + + /** + * Module info for this JAR + * + * @return Promise for module info + */ + public get moduleInfo(): Promise { + throw new Error('not yet implemented') + } + + /** @inheritdoc */ + public get manifestVersion(): string | null { + return this.manifest?.manifestVersion || null + } + + /** @inheritdoc */ + public get mainClass(): QualifiedClassName | null { + return this.manifest?.mainClass || null + } + + /** @inheritdoc */ + public get automaticModuleName(): string | null { + return this.manifest?.automaticModuleName || null + } + + /** + * Retrieve the value for a key in the JAR's manifest, or `null` if no such key is present. + * + * @param key Key to retrieve from this JAR's manifest + * @returns Value associated with the key, or `null` + */ + public async manifestValue(key: JarManifestKeyString): Promise { + return this.manifest?.get(key) || null + } + + /** + * Obtain a JAR entry at a specific path, or `null` if none exists. + * @param path Path to the entry to retrieve + * @return JAR entry at that path, or `null` + */ + public async entryAtPath(path: string): Promise { + return this._entries.get(path) || null + } + + /** + * Obtain a comiled class from the JAR entry at a specific path, or `null` if none exists. + * + * @param name Qualified name of the class to retrieve + * @return JAR class entry at that path, or `null` + */ + public async classAtName(name: string): Promise { + if (!name.includes('.')) throw new Error('invalid class name (must be qualified): ' + name) + const stanza = this._entries.get(qualifiedClassToJarPath(name)) + if (stanza) { + if (stanza.type === JarEntryType.CLASS) return stanza + throw new Error(`entry is not a class: ${name} (type: ${stanza.type})`) + } + return null + } + + /** + * Create an empty JAR builder. + * + * @return Empty JAR builder. + */ + static builder(): JarBuilder { + return new JarBuilder() + } + + /** + * Create a JAR file from raw contents. + * + * @param entries Entries in the JAR + * @param manifest Optional manifest to include in the JAR + * @return JAR file instance + */ + static fromRaw( + entries: JarEntry[], + manifest?: JarManifest, + options?: JarReaderOptions, + ): JarFile { + const opts = readerOptions(options) + const map: Map = new Map() + for (const entry of entries) { + map.set(entry.path, { + path: entry.path, + type: identifyEntry(entry.path), + entry: Promise.resolve(entry), + }) + } + return new JarFile( + opts, + map, + manifest || null, + ) + } + + /** + * Read a JAR file from the provided data buffer + * + * @param data Buffer of JAR file data + * @param options Optional reader options to apply to the operation + * @return JAR file instance + */ + static async fromData(data: Buffer, options?: JarReaderOptions): Promise { + const opts = readerOptions(options) + const { manifest, entries } = await inflateFromZip( + readZipFromData(data, buildPredicate(opts)), + ) + return new JarFile( + opts, + entries, + manifest || null, + ) + } + + /** + * Read a JAR file from the provided path on-disk + * + * @param path Path to the JAR file + * @param options Optional reader options to apply to the operation + * @return JAR file instance + */ + static async fromFile(path: string, options?: JarReaderOptions): Promise { + const opts = readerOptions(options) + const { manifest, entries } = await inflateFromZip( + readZipFromData(await require("fs/promises").readFile(path), buildPredicate(opts)), + ) + return new JarFile( + opts, + entries, + manifest || null, + ) + } + + /** + * Efficiently decompress and iterate over entries in the JAR matching the provided optional criteria. + * + * @param options Options governing how the iterator should behave. + * @return Async iterable which produces JAR entries. + */ + public async *entries(options?: JarEntryIterableOptions): AsyncIterable { + const opts: JarEntryIterableOptions = { + ...defaultEntryIterableOptions, + ...(options || {}), + } + + for await (const entry of this.contents()) { + if (this.eligible(opts, entry)) { + yield entry + } + } + } +} + +/** + * Build a JAR resource record. + * + * @param path Path to the resource within the JAR + * @param data Data for the resource + * @return JAR resource entry + */ +export function jarResource(path: string, data?: Buffer, compression?: JarCompression): JarResource { + return { + path, + size: data?.length || 0, + compression: compression || JarCompression.IDENTITY, + data: { + name: path, + read: async () => data ? ByteBuffer.wrap(data) : ByteBuffer.allocate(0), + } + } +} + +/** + * Build a JAR class entry. + * + * @param name Qualified name of the built class within the JAR + * @param data Serialized or loaded class data for the class + * @return JAR class entry + */ +export function jarClass(name: QualifiedClassName, data: Buffer | Promise): JarClassEntry { + return { + qualifiedName: name, + simpleName: qualifiedNameToSimpleName(name), + path: qualifiedClassToJarPath(name), + classfile: new Promise(async (accept, reject) => { + if (data instanceof Buffer) { + try { + accept(JavaClassFile.fromData(data)) + } catch (err) { + reject(err) + } + } else { + accept(await data) + } + }), + } +} + +/** + * Build a JAR service mapping entry. + * + * @param name Qualified name of the service interface + * @param compression Compression to use for the entry + * @param impl Implementation classes to include (optional) + * @return JAR service entry + */ +export function jarService( + name: QualifiedClassName, + ...impl: string[] +): JarServiceEntry { + return { + path: `META-INF/services/${name}`, + service: name, + impls: impl, + } +} + +/** + * Utility to obtain a JAR manifest builder + * + * @returns JAR manifest builder + */ +export function jarManifest(): JarManifestBuilder { + return JarManifest.builder() +} + + +// Default entrypoint. +export default JarFile; diff --git a/packages/java/java-manifest.ts b/packages/java/java-manifest.ts new file mode 100644 index 00000000..73dd360e --- /dev/null +++ b/packages/java/java-manifest.ts @@ -0,0 +1,556 @@ +/* + * Copyright (c) 2024 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under the License. + */ + +import { QualifiedClassName } from './java-model' + +/** + * Vendor string for this tool. + */ +export const vendorStamp = 'Elide Technologies, Inc.' + +/** + * `Created-By` JAR manifest value. + */ +export const createdByStamp = 'javatools/dev' + +/** + * Current version expected for JAR manifests, and used by this tool when generating manifests. + */ +export const currentManifestVersion = '1.1' + +/** + * Expected path within JARs where the manifest should be located + */ +export const manifestPath = 'META-INF/MANIFEST.MF' + +/** + * JAR Manifest Key + * + * Well-known JAR manifest keys. + */ +export enum JarManifestKey { + /** + * The `Manifest-Version` for a JAR. + */ + MANIFEST_VERSION = 'Manifest-Version', + + /** + * The `Automatic-Module-Name` assignment for a JAR. + */ + AUTOMATIC_MODULE_NAME = 'Automatic-Module-Name', + + /** + * The `Main-Class` for a JAR. + */ + MAIN_CLASS = 'Main-Class', + + /** + * The `Multi-Release` flag for MRJARs. + */ + MULTI_RELEASE = 'Multi-Release', + + /** + * The `Signature-Version` entry for a JAR. + */ + SIGNATURE_VERSION = 'Signature-Version', + + /** + * Embedded `Class-Path` for a JAR. + */ + CLASS_PATH = 'Class-Path', + + /** + * Refers to a "sealed" JAR; see below for more information: + * https://docs.oracle.com/en/java/javase/21/docs/specs/jar/jar.html#package-sealing + */ + SEALED = 'Sealed', + + /** + * Indicates what tool created the JAR. + */ + CREATED_BY = 'Created-By', + + /** + * Specifies a Java agent class to mount and use. + */ + LAUNCHER_AGENT_CLASS = 'Launcher-Agent-Class', + + /** + * Title of an extension implementation. + */ + IMPLEMENTATION_TITLE = 'Implementation-Title', + + /** + * Version of an extension implementation. + */ + IMPLEMENTATION_VERSION = 'Implementation-Version', + + /** + * Vendor of an extension implementation. + */ + IMPLEMENTATION_VENDOR = 'Implementation-Vendor', + + /** + * Title of a specification that the extension implements. + */ + SPECIFICATION_TITLE = 'Specification-Title', + + /** + * Version of a specification that the extension implements. + */ + SPECIFICATION_VERSION = 'Specification-Version', + + /** + * Vendor of a specification that the extension implements. + */ + SPECIFICATION_VENDOR = 'Specification-Vendor', + + /** + * SHA-1 digest provided for a JAR entry. + */ + SHA1_DIGEST = 'SHA1-Digest', + + /** + * SHA-256 digest provided for a JAR entry. + */ + SHA256_DIGEST = 'SHA256-Digest', + + /** + * "Magic" attribute for certain key-value pairs in JAR entries. + */ + MAGIC = 'Magic', +} + +/** + * JAR Manifest Key Type: Known or String + * + * Maps either pure strings or known keys to JAR manifest keys. + */ +export type JarManifestKeyString = (typeof JarManifestKey) | string + +/** + * JAR Manifest Data + * + * Describes the shape of raw JAR manifest data. + */ +export type JarManifestData = { + [Property in (keyof JarManifestKey | string)]: string | undefined; +} + +/** + * JAR Manifest: Qualified Data + * + * Describes data that can occur in a JAR manifest section, qualified by a class name. + */ +export type JarManifestQualifiedData = JarManifestData & { + /** + * Package path or class name. + */ + qualifierName: string; +} + +/** + * JAR Manifest: Raw Shape + * + * Describes the top-level raw shape of a JAR manifest, with its JAR-level clause and sections. + */ +export type RawJarManifest = JarManifestData & { + /** + * JAR manifest sections, if any. + */ + sections: JarManifestQualifiedData[]; +} + +/** + * JAR Manifest Baseline + * + * Base API interface for JAR manifest data, shared with other classes which provide proxied access to JAR manifest + * information (for instance, JAR file instances). + */ +export interface JarManifestBaseline { + /** + * Return the `Manifest-Version` specified for a JAR, or `null` if no version is defined. + */ + manifestVersion: string | null; + + /** + * Return the `Main-Class` specified for a JAR, or `null` if no main class is defined. + */ + mainClass: QualifiedClassName | null; + + /** + * Return the `Automatic-Module-Name` specified for a JAR, or `null` if no module name is defined. + */ + automaticModuleName: string | null; + + /** + * Return the `Multi-Release` flag specified for a JAR, or `null` if no flag is defined. + */ + multiRelease: boolean; +} + +/** + * JAR Manifest + * + * Describes the API provided for interpreted JAR manifests. Once a JAR manifest has been parsed and interpreted, + * it can be interrogated via the methods and getters provided by this interface. + */ +export interface JarManifest extends JarManifestBaseline { + /** + * Return the `Created-By` stamp present in the JAR manifest, if any, or `null`. + */ + createdBy: string | null; + + /** + * Return a raw JSON-style primitive representation of the JAR manifest. + */ + rawManifest: RawJarManifest; + + /** + * Serialize into a raw string representation, suitable to write to disk. + * + * @return Rendered string representation of the manifest. + */ + serializeManifest(): string; + + /** + * Retrieve the value of a top-level JAR manifest key, or return `null`. + * + * @param key Key to retrieve (top-level) from the JAR manifest + * @return Value of the key, or `null` if not present + */ + get(key: JarManifestKeyString): string | null; + + /** + * Retrieve the value of a qualified JAR manifest key, or return `null`. + * + * @param qualifierName Qualified name to retrieve from the JAR manifest + * @param key Key to retrieve from the qualified section + * @return Value of the key, or `null` if not present + */ + getQualified(qualifierName: string, key: JarManifestKeyString): string | null; + + /** + * Check if a top-level key is present in the JAR manifest. + * + * @param key Key to check for membership in the JAR manifest + */ + has(key: JarManifestKeyString): boolean; + + /** + * Check if a qualified key is present in the JAR manifest. + * + * @param qualifierName Qualified name to check for membership in the JAR manifest + * @param key Key to check for membership in the qualified section + */ + hasQualified(qualifierName: string, key: JarManifestKeyString): boolean; +} + +/** + * JAR Manifest Builder + * + * Implements a basic builder interface which can buffer and construct JAR manifest data into an immutable data + * structure, which can be serialized if needed. + * + * Parsing of JAR manifests happens via builders. All interpreted JAR manifest objects are immutable. + */ +export class JarManifestBuilder { + // Top-level JAR manifest keys. + private readonly topKeys: Map = new Map() + + // Qualified JAR manifest keys. + private readonly qualifiedKeys: Map> = new Map() + + /** + * Construct a new JAR manifest builder. + */ + public constructor() { + this.topKeys.set(JarManifestKey.MANIFEST_VERSION, currentManifestVersion) + this.topKeys.set(JarManifestKey.CREATED_BY, createdByStamp) + } + + /** + * Add a top-level key-value pair to the JAR manifest. + * + * @param key Key to add + * @param value Value to add + * @return Builder instance + */ + public add(key: JarManifestKeyString, value: string | boolean): this { + this.topKeys.set(key, value.toString()) + return this + } + + /** + * Add a qualified key-value pair to the JAR manifest. + * + * @param qualifierName Qualifier name to add + * @param key Key to add + * @param value Value to add + * @return Builder instance + */ + public addQualified(qualifierName: string, key: JarManifestKeyString, value: string): this { + if (!this.qualifiedKeys.has(qualifierName)) { + this.qualifiedKeys.set(qualifierName, new Map()) + } + + this.qualifiedKeys.get(qualifierName)?.set(key, value) + return this + } + + /** + * Serialize this builder into raw JAR manifest JSON data. + * + * @return Immutable JAR manifest data + */ + public serialize(): RawJarManifest { + return { + ...Object.fromEntries(this.topKeys), + sections: Array.from(this.qualifiedKeys).map(([qualifierName, data]) => ({ + qualifierName, + ...Object.fromEntries(data), + })), + } + } + + /** + * Build the builder into a serialized JAR manifest, and then interpret it as a JAR manifest. + * + * @return Interpreted JAR manifest + */ + public build(): JarManifest { + return ParsedJarManifest.fromRaw(this.serialize()) + } + + /** + * Build a JAR manifest builder from a raw JAR manifest structure. + * + * @param raw Raw JSON form of the manifest + * @return JAR manifest builder pre-loaded with the input data + */ + public static async fromRaw(raw: RawJarManifest): Promise { + const builder = new JarManifestBuilder() + for (const [key, value] of Object.entries(raw)) { + if (key === 'sections') { + for (const section of raw.sections) { + for (const [key, value] of Object.entries(section)) { + if (value) builder.addQualified(section.qualifierName, key, value) + } + } + } else if (value) { + builder.add(key, value as string) + } + } + return builder + } + + /** + * Parse JAR manifest information from the provided data. + * + * @param data Raw data to decode and parse as a JAR manifest + * @return Parsed JAR manifest data + * @throws If the provided data is not parseable as a JAR manifest + */ + public static async fromData(data: Buffer): Promise { + const decodedutf8 = data.toString('utf8') + const lines = decodedutf8.split(/\r?\n/) + const props: Partial = {} + const sections: Map = new Map() + let currentSection: string | null = null + let lineI = 0 + + let line: string | undefined; + let lastSeenProperty: string | undefined; + while (line = lines.shift()) { + lineI++ + + // if the line is empty or just a newline, skip it + if (!line || line === '\r' || line === '\n') continue + + if (!line.startsWith(' ')) { + if (line.startsWith('Name: ')) { + currentSection = line.slice(6) + sections[currentSection] = { qualifierName: currentSection } + } else { + const [key] = line.split(': ', 1) + const value = line.slice(key.length + 2) + + if (currentSection) { + sections[currentSection][key] = value.trim() + lastSeenProperty = key + } else { + props[key] = value.trim() + lastSeenProperty = key + } + } + } else { + // the line is indented, so we will need to append it to the last-seen property + if (!lastSeenProperty) { + throw new Error(`Unexpected indented line at line ${lineI}`) + } + if (!currentSection) { + props[lastSeenProperty] += line.trim() + } else { + sections[currentSection][lastSeenProperty] += line.trim() + } + } + } + return this.fromRaw(props as RawJarManifest) + } +} + +// Check a string value from a manifest. +function checkStringValue(manifest: RawJarManifest, key: JarManifestKeyString): string | null { + const value = manifest[key as string] + if (value === null || value === undefined) { + return null + } + if (!(typeof value === 'string') || value.length === 0) { + throw new Error(`Invalid value for key '${key}': '${value}'`) + } + return value +} + +// Check and cast a boolean value from a manifest. +function checkBooleanValue(manifest: RawJarManifest, key: JarManifestKeyString): boolean { + const value = manifest[key as string] + if (typeof value === 'boolean') { + return value + } else if (typeof value === 'string') { + if (value === 'true') return true + if (value === 'false') return false + } else if (value === null || value === undefined) { + return false + } + throw new Error(`Invalid value for key '${key}': '${value}'`) +} + +/** + * Parsed JAR Manifest + * + * Implements the JAR Manifest API for parsed JAR manifest data. + */ +export default class ParsedJarManifest implements JarManifest { + /** + * Private primary constructor. + * + * @param _raw Raw JAR manifest data, after parsing and potentially after checks + */ + private constructor(private readonly manifest: RawJarManifest) {} + + /** @inheritdoc */ + public get manifestVersion(): string | null { + return checkStringValue(this.manifest, JarManifestKey.MANIFEST_VERSION) + } + + /** @inheritdoc */ + public get mainClass(): QualifiedClassName | null { + return checkStringValue(this.manifest, JarManifestKey.MAIN_CLASS) + } + + /** @inheritdoc */ + public get createdBy(): string | null { + return checkStringValue(this.manifest, JarManifestKey.CREATED_BY) + } + + /** @inheritdoc */ + public get automaticModuleName(): string | null { + return checkStringValue(this.manifest, JarManifestKey.AUTOMATIC_MODULE_NAME) + } + + /** @inheritdoc */ + public get multiRelease(): boolean { + return checkBooleanValue(this.manifest, JarManifestKey.MULTI_RELEASE) + } + + /** @inheritdoc */ + public get rawManifest(): RawJarManifest { + return this.manifest + } + + /** @inheritdoc */ + public serializeManifest(): string { + let rendered = '' + for (const [key, value] of Object.entries(this.manifest)) { + if (key === 'sections') continue + rendered += `${key}: ${value}\n` + } + for (const data of Object.values(this.manifest.sections)) { + rendered += `Name: ${data.qualifierName}\n` + for (const [key, value] of Object.entries(data)) { + if (key === 'qualifierName') continue + rendered += `${key}: ${value}\n` + } + } + return rendered + } + + /** @inheritdoc */ + public get(key: JarManifestKeyString): string | null { + return checkStringValue(this.manifest, key) + } + + /** @inheritdoc */ + public getQualified(qualifierName: string, key: JarManifestKeyString): string | null { + const section = this.manifest.sections.find((section) => section.qualifierName === qualifierName) + if (!section) { + return null + } + const value = section[key as string] + if (value === null || value === undefined) { + return null + } + return value + } + + /** @inheritdoc */ + public has(key: JarManifestKeyString): boolean { + return this.manifest[key as string] !== undefined + } + + /** @inheritdoc */ + public hasQualified(qualifierName: string, key: JarManifestKeyString): boolean { + return this.manifest.sections.some((section) => section.qualifierName === qualifierName && section[key as string] !== undefined) + } + + /** + * Build an interpreted JAR from raw JAR manifest data. + * + * @param data Raw JSON-style primitive JAR manifest data + * @return Parsed JAR manifest data + * @throws If the provided data is not parseable as a JAR manifest + */ + public static fromRaw(data: RawJarManifest): JarManifest { + return new ParsedJarManifest(data) + } + + /** + * Create an empty JAR manifest builder. + * + * @return Empty builder + */ + public static builder(): JarManifestBuilder { + return new JarManifestBuilder() + } + + /** + * Parse JAR manifest information from the provided data. + * + * @param data Raw data to decode and parse as a JAR manifest + * @return Parsed JAR manifest data + * @throws If the provided data is not parseable as a JAR manifest + */ + public static async fromData(data: Buffer): Promise { + return (await JarManifestBuilder.fromData(data)).build() + } +} diff --git a/packages/java/java-model.ts b/packages/java/java-model.ts index d5a4ed31..95e186b5 100644 --- a/packages/java/java-model.ts +++ b/packages/java/java-model.ts @@ -11,75 +11,211 @@ * License for the specific language governing permissions and limitations under the License. */ +/** + * String type-alias for a simple, un-qualified class name. + */ +export type SimpleClassName = string + +/** + * String type-alias for a well-qualified Java class name. + */ +export type QualifiedClassName = string + +/** + * Java Toolchain Vendor + */ +export enum JavaToolchainVendor { + OPENJDK = 'openjdk', + GRAALVM = 'graalvm', + AZUL = 'azul' +} + +/** + * JVM Platform + * + * Enumerates JVM platforms (OS/arch pairs). + */ +export enum JvmPlatform { + LINUX_AMD64 = 'linux-amd64', + LINUX_AARCH64 = 'linux-aarch64', + DARWIN_AMD64 = 'darwin-amd64', + DARWIN_AARCH64 = 'darwin-aarch64', + WINDOWS_AMD64 = 'windows-amd64' +} + /** * JVM Target * * Enumerates JVM bytecode targets that are supported by this indexer tool */ export enum JvmTarget { - JDK_5 = "1.5", - JDK_6 = "1.6", - JDK_7 = "1.7", - JDK_8 = "1.8", - JDK_9 = "9", - JDK_10 = "10", - JDK_11 = "11", - JDK_12 = "12", - JDK_13 = "13", - JDK_14 = "14", - JDK_15 = "15", - JDK_16 = "16", - JDK_17 = "17", - JDK_18 = "18", - JDK_19 = "19", - JDK_20 = "20", - JDK_21 = "21", - JDK_22 = "22", - JDK_23 = "23", + JDK_5 = '1.5', + JDK_6 = '1.6', + JDK_7 = '1.7', + JDK_8 = '1.8', + JDK_9 = '9', + JDK_10 = '10', + JDK_11 = '11', + JDK_12 = '12', + JDK_13 = '13', + JDK_14 = '14', + JDK_15 = '15', + JDK_16 = '16', + JDK_17 = '17', + JDK_18 = '18', + JDK_19 = '19', + JDK_20 = '20', + JDK_21 = '21', + JDK_22 = '22', + JDK_23 = '23' +} + +const jvmTargetByVersion: Record = { + 5: JvmTarget.JDK_5, + 6: JvmTarget.JDK_6, + 7: JvmTarget.JDK_7, + 8: JvmTarget.JDK_8, + 9: JvmTarget.JDK_9, + 10: JvmTarget.JDK_10, + 11: JvmTarget.JDK_11, + 12: JvmTarget.JDK_12, + 13: JvmTarget.JDK_13, + 14: JvmTarget.JDK_14, + 15: JvmTarget.JDK_15, + 16: JvmTarget.JDK_16, + 17: JvmTarget.JDK_17, + 18: JvmTarget.JDK_18, + 19: JvmTarget.JDK_19, + 20: JvmTarget.JDK_20, + 21: JvmTarget.JDK_21, + 22: JvmTarget.JDK_22, + 23: JvmTarget.JDK_23 +} + +const jvmTargetByLevel: Record = { + 49: JvmTarget.JDK_5, + 50: JvmTarget.JDK_6, + 51: JvmTarget.JDK_7, + 52: JvmTarget.JDK_8, + 53: JvmTarget.JDK_9, + 54: JvmTarget.JDK_10, + 55: JvmTarget.JDK_11, + 56: JvmTarget.JDK_12, + 57: JvmTarget.JDK_13, + 58: JvmTarget.JDK_14, + 59: JvmTarget.JDK_15, + 60: JvmTarget.JDK_16, + 61: JvmTarget.JDK_17, + 62: JvmTarget.JDK_18, + 63: JvmTarget.JDK_19, + 64: JvmTarget.JDK_20, + 65: JvmTarget.JDK_21, + 66: JvmTarget.JDK_22, + 67: JvmTarget.JDK_23 } /** + * Obtain a `JvmTarget` for the provided JDK major version number * + * @param version JDK major version number + * @returns JVM target for the given JDK version */ -export type JavaModuleExport = { - package: string; - to?: string[]; -}; +export function jvmTargetForVersion(version: number): JvmTarget { + const target = jvmTargetByVersion[version] + if (!target) throw new Error(`Unsupported JDK version: ${version}`) + return target +} /** + * Obtain a `JvmTarget` for the provided class bytecode version * + * @param level Class bytecode level + * @returns JVM target for the given bytecode level */ -export type JavaModuleRequires = { - module: string; - static?: boolean; - transitive?: boolean; -}; +export function jvmTargetForLevel(level: number): JvmTarget { + const target = jvmTargetByLevel[level] + if (!target) throw new Error(`Unsupported bytecode level: ${level}`) + return target +} /** + * Obtain a JVM class bytecode level for the provided JVM target * + * @param target JVM target + * @returns Bytecode level for the target + */ +export function jvmLevelForTarget(target: JvmTarget): number { + return parseInt(target, 10) + 44 +} + +/** + * Describes an `export` declared in a JPMS module. + */ +export type JavaModuleExport = { + package: string + to: string[] +} + +/** + * Describes a `requires` declaration in a JPMS module. + */ +export type JavaModuleRequires = { + module: string + static: boolean + transitive: boolean +} + +/** + * Describes an `opens` declaration in a JPMS module. */ export type JavaModuleOpens = { - package: string; - to?: string[]; -}; + package: string + to: string[] +} /** - * + * Describes a `provides` declaration in a JPMS module. + */ +export type JavaModuleProvides = { + service: string + with: string[] +} + +/** + * Describes a `uses` declaration in a JPMS module. + */ +export type JavaModuleUses = { + service: string +} + +/** + * Flags which can be set on a Java Module definition. + */ +export type JavaModuleFlags = { + open: boolean +} + +/** + * Describes a JPMS module. */ export type JavaModuleInfo = { - name: string; - open: boolean; - requires: JavaModuleRequires[]; - exports: JavaModuleExport[]; - opens: JavaModuleOpens[]; -}; + name: string + version?: string + main?: string + flags: JavaModuleFlags + requires: JavaModuleRequires[] + exports: JavaModuleExport[] + opens: JavaModuleOpens[] + provides: JavaModuleProvides[] + uses: JavaModuleUses[] +} /** - * + * Describes Java release feature attributes. */ export type ReleaseFeatures = { - minimumTarget?: JvmTarget; - definedTargets?: JvmTarget[]; - module?: JavaModuleInfo; - multiRelease?: boolean; -}; + minimumTarget?: JvmTarget + definedTargets?: JvmTarget[] + module?: JavaModuleInfo + multiRelease?: boolean +} diff --git a/packages/java/jar-reader.ts b/packages/java/javaclasses/attribute.ts similarity index 83% rename from packages/java/jar-reader.ts rename to packages/java/javaclasses/attribute.ts index 77a13f95..fa6b5adc 100644 --- a/packages/java/jar-reader.ts +++ b/packages/java/javaclasses/attribute.ts @@ -11,12 +11,11 @@ * License for the specific language governing permissions and limitations under the License. */ -// @ts-ignore -import { JavaClassFile } from "./classfile-parser"; - /** + * Attribute Names * + * Enumerates Java Class attribute names of note. */ -export class JarFile { - // +export enum AttributeName { + MODULE = 'Module' } diff --git a/packages/java/javaclasses/constant-pool.ts b/packages/java/javaclasses/constant-pool.ts new file mode 100644 index 00000000..0a25a888 --- /dev/null +++ b/packages/java/javaclasses/constant-pool.ts @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under the License. + */ + +import { ClassFile } from './java-class-reader' + +export default { + /** + * Get IEEE 754 float from constant_pool + * + * See https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4.4 + */ + getFloat: function (classFile: ClassFile, index: number): number { + return this.u32ToFloat(classFile.constant_pool[index].bytes) + }, + + /** + * Converts 32-bit integer to IEEE 754 float as described in + * https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4.4 + */ + u32ToFloat: function (bits: number): number { + if (bits === 0x7f800000) { + return Number.POSITIVE_INFINITY + } + if (bits === 0xff800000) { + return Number.NEGATIVE_INFINITY + } + if ((bits >= 0xff800001 && bits <= 0xffffffff) || (bits >= 0x7f800001 && bits <= 0x7fffffff)) { + return Number.NaN + } + + let s = bits >> 31 === 0 ? 1 : -1 + let e = (bits >> 23) & 0xff + let m = e === 0 ? (bits & 0x7fffff) << 1 : (bits & 0x7fffff) | 0x800000 + return s * m * Math.pow(2, e - 150) + }, + + bytesToString: function (bytes: number[]): string { + return String.fromCharCode.apply(null, bytes) + }, + + getString: function (classFile: ClassFile, index: number): string { + let cp_entry = classFile.constant_pool[index] + return this.bytesToString(cp_entry.bytes) + } +} diff --git a/packages/java/javaclasses/constant-type.ts b/packages/java/javaclasses/constant-type.ts index 58fd6389..14d9e6e5 100644 --- a/packages/java/javaclasses/constant-type.ts +++ b/packages/java/javaclasses/constant-type.ts @@ -40,5 +40,5 @@ export enum ConstantType { DYNAMIC = 17, INVOKE_DYNAMIC = 18, MODULE = 19, - PACKAGE = 20, + PACKAGE = 20 } diff --git a/packages/java/javaclasses/instruction-parser.ts b/packages/java/javaclasses/instruction-parser.ts index 0abf0d3a..46944a3d 100644 --- a/packages/java/javaclasses/instruction-parser.ts +++ b/packages/java/javaclasses/instruction-parser.ts @@ -18,342 +18,180 @@ * Licensed under the MIT License. See LICENSE file in the project root for full license information. */ -import Opcode from "./opcode"; +import Opcode from './opcode' // -1 = variable length // index = opcode +// prettier-ignore const opcodeOperandCount = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 1, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, -1, -1, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 4, 4, 2, 1, 2, 0, 0, 2, 2, 0, - 0, -1, 3, 2, 2, 4, 4, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 1, 2, + 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, -1, -1, 0, 0, 0, 0, 0, 0, 2, + 2, 2, 2, 2, 2, 2, 4, 4, 2, 1, 2, 0, 0, 2, 2, 0, 0, -1, 3, + 2, 2, 4, 4, 0 ]; // index = opcode +// prettier-ignore const opcodeMnemonics = [ - "nop", - "aconst_null", - "iconst_m1", - "iconst_0", - "iconst_1", - "iconst_2", - "iconst_3", - "iconst_4", - "iconst_5", - "lconst_0", - "lconst_1", - "fconst_0", - "fconst_1", - "fconst_2", - "dconst_0", - "dconst_1", - "bipush", - "sipush", - "ldc", - "ldc_w", - "ldc2_w", - "iload", - "lload", - "fload", - "dload", - "aload", - "iload_0", - "iload_1", - "iload_2", - "iload_3", - "lload_0", - "lload_1", - "lload_2", - "lload_3", - "fload_0", - "fload_1", - "fload_2", - "fload_3", - "dload_0", - "dload_1", - "dload_2", - "dload_3", - "aload_0", - "aload_1", - "aload_2", - "aload_3", - "iaload", - "laload", - "faload", - "daload", - "aaload", - "baload", - "caload", - "saload", - "istore", - "lstore", - "fstore", - "dstore", - "astore", - "istore_0", - "istore_1", - "istore_2", - "istore_3", - "lstore_0", - "lstore_1", - "lstore_2", - "lstore_3", - "fstore_0", - "fstore_1", - "fstore_2", - "fstore_3", - "dstore_0", - "dstore_1", - "dstore_2", - "dstore_3", - "astore_0", - "astore_1", - "astore_2", - "astore_3", - "iastore", - "lastore", - "fastore", - "dastore", - "aastore", - "bastore", - "castore", - "sastore", - "pop", - "pop2", - "dup", - "dup_x1", - "dup_x2", - "dup2", - "dup2_x1", - "dup2_x2", - "swap", - "iadd", - "ladd", - "fadd", - "dadd", - "isub", - "lsub", - "fsub", - "dsub", - "imul", - "lmul", - "fmul", - "dmul", - "idiv", - "ldiv", - "fdiv", - "ddiv", - "irem", - "lrem", - "frem", - "drem", - "ineg", - "lneg", - "fneg", - "dneg", - "ishl", - "lshl", - "ishr", - "lshr", - "iushr", - "lushr", - "iand", - "land", - "ior", - "lor", - "ixor", - "lxor", - "iinc", - "i2l", - "i2f", - "i2d", - "l2i", - "l2f", - "l2d", - "f2i", - "f2l", - "f2d", - "d2i", - "d2l", - "d2f", - "i2b", - "i2c", - "i2s", - "lcmp", - "fcmpl", - "fcmpg", - "dcmpl", - "dcmpg", - "ifeq", - "ifne", - "iflt", - "ifge", - "ifgt", - "ifle", - "if_icmpeq", - "if_icmpne", - "if_icmplt", - "if_icmpge", - "if_icmpgt", - "if_icmple", - "if_acmpeq", - "if_acmpne", - "goto", - "jsr", - "ret", - "tableswitch", - "lookupswitch", - "ireturn", - "lreturn", - "freturn", - "dreturn", - "areturn", - "return", - "getstatic", - "putstatic", - "getfield", - "putfield", - "invokevirtual", - "invokespecial", - "invokestatic", - "invokeinterface", - "invokedynamic", - "new", - "newarray", - "anewarray", - "arraylength", - "athrow", - "checkcast", - "instanceof", - "monitorenter", - "monitorexit", - "wide", - "multianewarray", - "ifnull", - "ifnonnull", - "goto_w", - "jsr_w", - "breakpoint", + 'nop', 'aconst_null', 'iconst_m1', 'iconst_0', 'iconst_1', 'iconst_2', 'iconst_3', 'iconst_4', 'iconst_5', 'lconst_0', + 'lconst_1', 'fconst_0', 'fconst_1', 'fconst_2', 'dconst_0', 'dconst_1', 'bipush', 'sipush', 'ldc', 'ldc_w', + 'ldc2_w', 'iload', 'lload', 'fload', 'dload', 'aload', 'iload_0', 'iload_1', 'iload_2', 'iload_3', + 'lload_0', 'lload_1', 'lload_2', 'lload_3', 'fload_0', 'fload_1', 'fload_2', 'fload_3', 'dload_0', 'dload_1', + 'dload_2', 'dload_3', 'aload_0', 'aload_1', 'aload_2', 'aload_3', 'iaload', 'laload', 'faload', 'daload', + 'aaload', 'baload', 'caload', 'saload', 'istore', 'lstore', 'fstore', 'dstore', 'astore', 'istore_0', + 'istore_1', 'istore_2', 'istore_3', 'lstore_0', 'lstore_1', 'lstore_2', 'lstore_3', 'fstore_0', 'fstore_1', 'fstore_2', + 'fstore_3', 'dstore_0', 'dstore_1', 'dstore_2', 'dstore_3', 'astore_0', 'astore_1', 'astore_2', 'astore_3', 'iastore', + 'lastore', 'fastore', 'dastore', 'aastore', 'bastore', 'castore', 'sastore', 'pop', 'pop2', 'dup', 'dup_x1', 'dup_x2', + 'dup2', 'dup2_x1', 'dup2_x2', 'swap', 'iadd', 'ladd', 'fadd', 'dadd', 'isub', 'lsub', 'fsub', 'dsub', 'imul', 'lmul', 'fmul', + 'dmul', 'idiv', 'ldiv', 'fdiv', 'ddiv', 'irem', 'lrem', 'frem', 'drem', 'ineg', 'lneg', 'fneg', 'dneg', 'ishl', 'lshl', + 'ishr', 'lshr', 'iushr', 'lushr', 'iand', 'land', 'ior', 'lor', 'ixor', 'lxor', 'iinc', 'i2l', 'i2f', 'i2d', 'l2i', 'l2f', + 'l2d', 'f2i', 'f2l', 'f2d', 'd2i', 'd2l', 'd2f', 'i2b', 'i2c', 'i2s', 'lcmp', 'fcmpl', 'fcmpg', 'dcmpl', 'dcmpg', + 'ifeq', 'ifne', 'iflt', 'ifge', 'ifgt', 'ifle', 'if_icmpeq', 'if_icmpne', 'if_icmplt', 'if_icmpge', 'if_icmpgt', + 'if_icmple', 'if_acmpeq', 'if_acmpne', 'goto', 'jsr', 'ret', 'tableswitch', 'lookupswitch', 'ireturn', 'lreturn', + 'freturn', 'dreturn', 'areturn', 'return', 'getstatic', 'putstatic', 'getfield', 'putfield', 'invokevirtual', + 'invokespecial', 'invokestatic', 'invokeinterface', 'invokedynamic', 'new', 'newarray', 'anewarray', 'arraylength', + 'athrow', 'checkcast', 'instanceof', 'monitorenter', 'monitorexit', 'wide', 'multianewarray', 'ifnull', 'ifnonnull', + 'goto_w', 'jsr_w', 'breakpoint' ]; +/** + * Instruction + * + * Represents a JVM instruction. + */ class Instruction { constructor( readonly opcode: Opcode, readonly operands: number[], - readonly bytecodeOffset: number, + readonly bytecodeOffset: number ) { - if (typeof opcode !== "number") throw TypeError("opcode must be a number"); - if (!Array.isArray(operands)) throw TypeError("operands must be an array"); + if (typeof opcode !== 'number') throw TypeError('opcode must be a number') + if (!Array.isArray(operands)) throw TypeError('operands must be an array') } - toString() { + /** + * Produce a clean string representation. + * + * @returns String representation of the instruction. + */ + toString(): string { const opcodeMnemonic = - opcodeMnemonics[this.opcode] || (this.opcode === 0xfe ? "impdep1" : this.opcode === 0xff ? "impdep2" : undefined); - return `Instruction { opcode: ${opcodeMnemonic}, operands: [${this.operands}] }`; + opcodeMnemonics[this.opcode] || (this.opcode === 0xfe ? 'impdep1' : this.opcode === 0xff ? 'impdep2' : undefined) + return `Instruction { opcode: ${opcodeMnemonic}, operands: [${this.operands}] }` } } +/** + * Instruction Parser + * + * Parses raw bytecode into Instruction objects and vice-versa. + */ class InstructionParser { /** * Converts Instruction objects into raw bytecode. * - * @param {Instruction[]} instruction - Instructions to convert. + * @param instructions - Instructions to convert. * @see {@link https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5} - * @returns {Number[]} + * @returns Serialized bytecode */ static toBytecode(instructions: Instruction[]): number[] { - if (!Array.isArray(instructions)) throw TypeError("instructions must be an array."); + if (!Array.isArray(instructions)) throw TypeError('instructions must be an array.') - const bytecode: Opcode[] = []; + const bytecode: Opcode[] = [] for (const { opcode, operands } of instructions) { - bytecode.push(opcode); + bytecode.push(opcode) if (opcode === Opcode.TABLESWITCH || opcode === Opcode.LOOKUPSWITCH) { - let padding = bytecode.length % 4 ? 4 - (bytecode.length % 4) : 0; + let padding = bytecode.length % 4 ? 4 - (bytecode.length % 4) : 0 while (padding-- > 0) { - bytecode.push(0); + bytecode.push(0) } } // we assume we are given valid operands - bytecode.push(...operands); + bytecode.push(...operands) } - return bytecode; + return bytecode } /** * Converts raw bytecode into Instruction objects. * - * @param {number[]} bytecode - An array of bytes containing the jvm bytecode. + * @param bytecode - An array of bytes containing the jvm bytecode. * @see {@link https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5} - * @returns {Instruction[]} + * @returns Parsed instructions */ - static fromBytecode(bytecode: number[]) { - if (!Array.isArray(bytecode)) throw TypeError("bytecode must be an array of bytes."); - const instructions: Instruction[] = []; - let offset = 0; + static fromBytecode(bytecode: number[]): Instruction[] { + if (!Array.isArray(bytecode)) throw TypeError('bytecode must be an array of bytes.') + const instructions: Instruction[] = [] + let offset = 0 while (offset < bytecode.length) { - const bytecodeOffset = offset; - const opcode = bytecode[offset++]; - let numOperandBytes; + const bytecodeOffset = offset + const opcode = bytecode[offset++] + let numOperandBytes switch (opcode) { // https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.lookupswitch case Opcode.LOOKUPSWITCH: { - const padding = offset % 4 ? 4 - (offset % 4) : 0; - offset += padding; // skip padding + const padding = offset % 4 ? 4 - (offset % 4) : 0 + offset += padding // skip padding const npairs = (bytecode[offset + 4] << 24) | (bytecode[offset + 5] << 16) | (bytecode[offset + 6] << 8) | - bytecode[offset + 7]; + bytecode[offset + 7] - numOperandBytes = 8 + npairs * 8; - break; + numOperandBytes = 8 + npairs * 8 + break } // https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.tableswitch case Opcode.TABLESWITCH: { - const padding = offset % 4 ? 4 - (offset % 4) : 0; - offset += padding; // skip padding + const padding = offset % 4 ? 4 - (offset % 4) : 0 + offset += padding // skip padding const low = (bytecode[offset + 4] << 24) | (bytecode[offset + 5] << 16) | (bytecode[offset + 6] << 8) | - bytecode[offset + 7]; + bytecode[offset + 7] const high = (bytecode[offset + 8] << 24) | (bytecode[offset + 9] << 16) | (bytecode[offset + 10] << 8) | - bytecode[offset + 11]; - const numJumpOffsets = high - low + 1; - numOperandBytes = 3 * 4 + numJumpOffsets * 4; - break; + bytecode[offset + 11] + const numJumpOffsets = high - low + 1 + numOperandBytes = 3 * 4 + numJumpOffsets * 4 + break } // https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.wide case Opcode.WIDE: - numOperandBytes = bytecode[offset] === Opcode.IINC ? 5 : 3; - break; + numOperandBytes = bytecode[offset] === Opcode.IINC ? 5 : 3 + break default: - numOperandBytes = opcodeOperandCount[opcode]; - if (numOperandBytes === undefined) throw Error(`Unexpected opcode: ${opcode}`); - break; + numOperandBytes = opcodeOperandCount[opcode] + if (numOperandBytes === undefined) throw Error(`Unexpected opcode: ${opcode}`) + break } - const operands = bytecode.slice(offset, offset + numOperandBytes); - const instruction = new Instruction(opcode, operands, bytecodeOffset); + const operands = bytecode.slice(offset, offset + numOperandBytes) + const instruction = new Instruction(opcode, operands, bytecodeOffset) - instructions.push(instruction); - offset += numOperandBytes; + instructions.push(instruction) + offset += numOperandBytes } - return instructions; + return instructions } } -export { Instruction, InstructionParser }; +export { Instruction, InstructionParser } diff --git a/packages/java/javaclasses/java-class-reader.ts b/packages/java/javaclasses/java-class-reader.ts index 95c66e87..4af3e2ea 100644 --- a/packages/java/javaclasses/java-class-reader.ts +++ b/packages/java/javaclasses/java-class-reader.ts @@ -18,66 +18,64 @@ * Licensed under the MIT License. See LICENSE file in the project root for full license information. */ -import ByteBuffer from "bytebuffer"; -import { ConstantType } from "./constant-type"; - -type MemberInfo = { - access_flags: number; - name_index: number; - descriptor_index: number; - attributes_count: number; - attributes: any[]; -}; - -type TypeInfo = { - tag: number; - cpool_index?: number; - offset?: number; -}; +import ByteBuffer from 'bytebuffer' +import { ConstantType } from './constant-type' +import { AttributeInfo, MemberInfo, TypeInfo } from './java-class-types' + +/** + * Magic value used to identify class files. + */ +export const MAGIC = 0xcafebabe /** * Parsed Class File */ export class ClassFile { - readonly minor_version: number; - readonly major_version: number; - readonly constant_pool_count: number; - readonly constant_pool: any[]; - readonly access_flags: number; - readonly this_class: number; - readonly super_class: number; - readonly interfaces_count: number; - readonly interfaces: any[]; - readonly fields_count: number; - readonly fields: any[]; - readonly methods_count: number; - readonly methods: any[]; - readonly attributes_count: number; - readonly attributes: any[]; + readonly minor_version: number + readonly major_version: number + readonly constant_pool_count: number + readonly constant_pool: any[] + readonly access_flags: number + readonly this_class: number + readonly super_class: number + readonly interfaces_count: number + readonly interfaces: any[] + readonly fields_count: number + readonly fields: any[] + readonly methods_count: number + readonly methods: any[] + readonly attributes_count: number + readonly attributes: any[] // nothing yet - private constructor(private readonly buf: ByteBuffer) { - if (buf.readUint32() !== 0xcafebabe) throw Error("Invalid MAGIC value"); - - this.minor_version = buf.readUint16(); - this.major_version = buf.readUint16(); - this.constant_pool_count = buf.readUint16(); - this.constant_pool = this._readConstantPool(this.constant_pool_count - 1); - this.access_flags = buf.readUint16(); - this.this_class = buf.readUint16(); - this.super_class = buf.readUint16(); - this.interfaces_count = buf.readUint16(); - this.interfaces = this._readInterfaces(this.interfaces_count); - this.fields_count = buf.readUint16(); - this.fields = this._readMemberInfoArray(this.fields_count); - this.methods_count = buf.readUint16(); - this.methods = this._readMemberInfoArray(this.methods_count); - this.attributes_count = buf.readUint16(); - this.attributes = this._readAttributeInfoArray(this.attributes_count); + private constructor(buf: ByteBuffer) { + if (buf.readUint32() !== MAGIC) throw Error('Invalid MAGIC value') + + this.minor_version = buf.readUint16() + this.major_version = buf.readUint16() + this.constant_pool_count = buf.readUint16() + this.constant_pool = ClassFile._readConstantPool(buf, this.constant_pool_count - 1) + this.access_flags = buf.readUint16() + this.this_class = buf.readUint16() + this.super_class = buf.readUint16() + this.interfaces_count = buf.readUint16() + this.interfaces = ClassFile._readInterfaces(buf, this.interfaces_count) + this.fields_count = buf.readUint16() + this.fields = ClassFile._readMemberInfoArray(buf, this.fields_count, this.constant_pool) + this.methods_count = buf.readUint16() + this.methods = ClassFile._readMemberInfoArray(buf, this.methods_count, this.constant_pool) + this.attributes_count = buf.readUint16() + this.attributes = ClassFile._readAttributeInfoArray(buf, this.attributes_count, this.constant_pool) } + /** + * Parse data representing a compiler Java class + * + * @param buf Byte buffer of data to parse the class from + * @returns Parsed class information + */ static fromData(buf: ByteBuffer): ClassFile { - return new ClassFile(buf); + return new ClassFile(buf) } /** @@ -86,54 +84,54 @@ export class ClassFile { * https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.6 * https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.5 */ - _readMemberInfoArray(count: number) { - const members = new Array(count); + static _readMemberInfoArray(buf: ByteBuffer, count: number, constantPool: any[]) { + const members = new Array(count) for (let i = 0; i < count; i++) { const memberInfo: Partial = { - access_flags: this.buf.readUint16(), - name_index: this.buf.readUint16(), - descriptor_index: this.buf.readUint16(), - attributes_count: this.buf.readUint16(), - }; - memberInfo.attributes = this._readAttributeInfoArray(memberInfo.attributes_count as number); - members[i] = memberInfo; + access_flags: buf.readUint16(), + name_index: buf.readUint16(), + descriptor_index: buf.readUint16(), + attributes_count: buf.readUint16() + } + memberInfo.attributes = this._readAttributeInfoArray(buf, memberInfo.attributes_count as number, constantPool) + members[i] = memberInfo } - return members; + return members } - _readAttributeInfoArray(attributes_count: number) { - const attributes = new Array(attributes_count); + static _readAttributeInfoArray(buf: ByteBuffer, attributes_count: number, constantPool: any[]) { + const attributes = new Array(attributes_count) for (let i = 0; i < attributes_count; i++) { - attributes[i] = this._readAttributeInfo(); + attributes[i] = this._readAttributeInfo(buf, constantPool) } - return attributes; + return attributes } /** * Reads the "verification_type_info" structure. * https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.4 */ - _readVerificationTypeInfo() { + static _readVerificationTypeInfo(buf: ByteBuffer) { const type_info: Partial = { - tag: this.buf.readUint8(), - }; + tag: buf.readUint8() + } if (type_info.tag === 7) { - type_info.cpool_index = this.buf.readUint16(); + type_info.cpool_index = buf.readUint16() } else if (type_info.tag === 8) { - type_info.offset = this.buf.readUint16(); + type_info.offset = buf.readUint16() } - return type_info; + return type_info } /** * Reads the "type_annotation" structure * https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.20 */ - _readTypeAnnotation() { + static _readTypeAnnotation(buf: ByteBuffer) { const type_annotation: any = { - target_type: this.buf.readUint8(), - target_info: {}, - }; + target_type: buf.readUint8(), + target_info: {} + } // Reads the "target_info" union. // https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.20-400 @@ -141,67 +139,67 @@ export class ClassFile { // type_parameter_target case 0x00: case 0x01: - type_annotation.target_info.type_parameter_index = this.buf.readUint8(); - break; + type_annotation.target_info.type_parameter_index = buf.readUint8() + break // supertype_target case 0x10: - type_annotation.target_info.supertype_index = this.buf.readUint16(); - break; + type_annotation.target_info.supertype_index = buf.readUint16() + break // type_parameter_bound_target case 0x11: case 0x12: - type_annotation.target_info.type_parameter_index = this.buf.readUint8(); - type_annotation.target_info.bound_index = this.buf.readUint8(); - break; + type_annotation.target_info.type_parameter_index = buf.readUint8() + type_annotation.target_info.bound_index = buf.readUint8() + break // empty_target case 0x13: case 0x14: case 0x15: // empty - break; + break // formal_parameter_target case 0x16: - type_annotation.target_info.formal_parameter_index = this.buf.readUint8(); - break; + type_annotation.target_info.formal_parameter_index = buf.readUint8() + break // throws_target case 0x17: - type_annotation.target_info.throws_type_index = this.buf.readUint16(); - break; + type_annotation.target_info.throws_type_index = buf.readUint16() + break // localvar_target case 0x40: case 0x41: { - type_annotation.target_info.table_length = this.buf.readUint16(); - type_annotation.target_info.table = new Array(type_annotation.target_info.table_length); + type_annotation.target_info.table_length = buf.readUint16() + type_annotation.target_info.table = new Array(type_annotation.target_info.table_length) for (let i = 0; i < type_annotation.target_info.table_length; i++) { const table_entry = { - start_pc: this.buf.readUint16(), - length: this.buf.readUint16(), - index: this.buf.readUint16(), - }; - type_annotation.target_info.table[i] = table_entry; + start_pc: buf.readUint16(), + length: buf.readUint16(), + index: buf.readUint16() + } + type_annotation.target_info.table[i] = table_entry } - break; + break } // catch_target case 0x42: - type_annotation.target_info.exception_table_index = this.buf.readUint16(); - break; + type_annotation.target_info.exception_table_index = buf.readUint16() + break // offset_target case 0x43: case 0x44: case 0x45: case 0x46: - type_annotation.target_info.offset = this.buf.readUint16(); - break; + type_annotation.target_info.offset = buf.readUint16() + break // type_argument_target case 0x47: @@ -209,134 +207,134 @@ export class ClassFile { case 0x49: case 0x4a: case 0x4b: - type_annotation.target_info.offset = this.buf.readUint16(); - type_annotation.target_info.type_argument_index = this.buf.readUint8(); - break; + type_annotation.target_info.offset = buf.readUint16() + type_annotation.target_info.type_argument_index = buf.readUint8() + break default: - throw Error(`Unexpected target_type: ${type_annotation.target_type}`); + throw Error(`Unexpected target_type: ${type_annotation.target_type}`) } // Reads "type_path" structure - type_annotation.type_path = { path_length: this.buf.readUint8() }; - type_annotation.type_path.path = new Array(type_annotation.type_path.path_length); + type_annotation.type_path = { path_length: buf.readUint8() } + type_annotation.type_path.path = new Array(type_annotation.type_path.path_length) for (let i = 0; i < type_annotation.type_path.path_length; i++) { type_annotation.type_path.path[i] = { - type_path_kind: this.buf.readUint8(), - type_argument_index: this.buf.readUint8(), - }; + type_path_kind: buf.readUint8(), + type_argument_index: buf.readUint8() + } } - type_annotation.type_index = this.buf.readUint16(); - type_annotation.num_element_value_pairs = this.buf.readUint16(); - type_annotation.element_value_pairs = new Array(type_annotation.num_element_value_pairs); + type_annotation.type_index = buf.readUint16() + type_annotation.num_element_value_pairs = buf.readUint16() + type_annotation.element_value_pairs = new Array(type_annotation.num_element_value_pairs) for (let i = 0; i < type_annotation.num_element_value_pairs; i++) { type_annotation.element_value_pairs[i] = { - element_name_index: this.buf.readUint16(), - element_value: this._readElementValue(), - }; + element_name_index: buf.readUint16(), + element_value: this._readElementValue(buf) + } } - return type_annotation; + return type_annotation } /** * Reads the "attribute_info" structure * https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7 */ - _readAttributeInfo() { - const attribute: any = { - attribute_name_index: this.buf.readUint16(), - attribute_length: this.buf.readUint32(), - }; + static _readAttributeInfo(buf: ByteBuffer, constantPool: any[]) { + const attribute: AttributeInfo = { + attribute_name_index: buf.readUint16(), + attribute_length: buf.readUint32() + } - const attributeNameBytes = this.constant_pool[attribute.attribute_name_index].bytes; - const attributeName = String.fromCharCode.apply(null, attributeNameBytes); + const attributeNameBytes = constantPool[attribute.attribute_name_index].bytes + const attributeName = String.fromCharCode.apply(null, attributeNameBytes) switch (attributeName) { - case "Deprecated": - case "Synthetic": - break; + case 'Deprecated': + case 'Synthetic': + break - case "RuntimeInvisibleAnnotations": - case "RuntimeVisibleAnnotations": { - attribute.num_annotations = this.buf.readUint16(); - attribute.annotations = new Array(attribute.num_annotations); + case 'RuntimeInvisibleAnnotations': + case 'RuntimeVisibleAnnotations': { + attribute.num_annotations = buf.readUint16() + attribute.annotations = new Array(attribute.num_annotations) for (let i = 0; i < attribute.num_annotations; i++) { - attribute.annotations[i] = this._readAttributeAnnotation(); + attribute.annotations[i] = this._readAttributeAnnotation(buf) } - break; + break } - case "InnerClasses": { - attribute.number_of_classes = this.buf.readUint16(); - attribute.classes = new Array(attribute.number_of_classes); + case 'InnerClasses': { + attribute.number_of_classes = buf.readUint16() + attribute.classes = new Array(attribute.number_of_classes) for (let i = 0; i < attribute.number_of_classes; i++) { attribute.classes[i] = { - inner_class_info_index: this.buf.readUint16(), - outer_class_info_index: this.buf.readUint16(), - inner_name_index: this.buf.readUint16(), - inner_class_access_flags: this.buf.readUint16(), - }; + inner_class_info_index: buf.readUint16(), + outer_class_info_index: buf.readUint16(), + inner_name_index: buf.readUint16(), + inner_class_access_flags: buf.readUint16() + } } - break; + break } - case "LocalVariableTable": { - attribute.local_variable_table_length = this.buf.readUint16(); - attribute.local_variable_table = new Array(attribute.local_variable_table_length); + case 'LocalVariableTable': { + attribute.local_variable_table_length = buf.readUint16() + attribute.local_variable_table = new Array(attribute.local_variable_table_length) for (let i = 0; i < attribute.local_variable_table_length; i++) { attribute.local_variable_table[i] = { - start_pc: this.buf.readUint16(), - length: this.buf.readUint16(), - name_index: this.buf.readUint16(), - descriptor_index: this.buf.readUint16(), - index: this.buf.readUint16(), - }; + start_pc: buf.readUint16(), + length: buf.readUint16(), + name_index: buf.readUint16(), + descriptor_index: buf.readUint16(), + index: buf.readUint16() + } } - break; + break } - case "LocalVariableTypeTable": { - attribute.local_variable_type_table_length = this.buf.readUint16(); - attribute.local_variable_type_table = new Array(attribute.local_variable_type_table_length); + case 'LocalVariableTypeTable': { + attribute.local_variable_type_table_length = buf.readUint16() + attribute.local_variable_type_table = new Array(attribute.local_variable_type_table_length) for (let i = 0; i < attribute.local_variable_type_table_length; i++) { attribute.local_variable_type_table[i] = { - start_pc: this.buf.readUint16(), - length: this.buf.readUint16(), - name_index: this.buf.readUint16(), - signature_index: this.buf.readUint16(), - index: this.buf.readUint16(), - }; + start_pc: buf.readUint16(), + length: buf.readUint16(), + name_index: buf.readUint16(), + signature_index: buf.readUint16(), + index: buf.readUint16() + } } - break; + break } - case "RuntimeInvisibleParameterAnnotations": - case "RuntimeVisibleParameterAnnotations": { - attribute.num_parameters = this.buf.readUint8(); - attribute.parameter_annotations = new Array(attribute.num_parameters); + case 'RuntimeInvisibleParameterAnnotations': + case 'RuntimeVisibleParameterAnnotations': { + attribute.num_parameters = buf.readUint8() + attribute.parameter_annotations = new Array(attribute.num_parameters) for (let parameterIndex = 0; parameterIndex < attribute.num_parameters; parameterIndex++) { - const parameter_annotation: any = { num_annotations: this.buf.readUint16() }; - parameter_annotation.annotations = new Array(parameter_annotation.num_annotations); + const parameter_annotation: any = { num_annotations: buf.readUint16() } + parameter_annotation.annotations = new Array(parameter_annotation.num_annotations) for (let annotationIndex = 0; annotationIndex < parameter_annotation.num_annotations; annotationIndex++) { - parameter_annotation.annotations[annotationIndex] = this._readAttributeAnnotation(); + parameter_annotation.annotations[annotationIndex] = this._readAttributeAnnotation(buf) } - attribute.parameter_annotations[parameterIndex] = parameter_annotation; + attribute.parameter_annotations[parameterIndex] = parameter_annotation } - break; + break } - case "BootstrapMethods": { - attribute.num_bootstrap_methods = this.buf.readUint16(); - attribute.bootstrap_methods = new Array(attribute.num_bootstrap_methods); + case 'BootstrapMethods': { + attribute.num_bootstrap_methods = buf.readUint16() + attribute.bootstrap_methods = new Array(attribute.num_bootstrap_methods) for ( let bootstrapMethodIndex = 0; @@ -344,173 +342,174 @@ export class ClassFile { bootstrapMethodIndex++ ) { const bootstrap_method: any = { - bootstrap_method_ref: this.buf.readUint16(), - num_bootstrap_arguments: this.buf.readUint16(), - }; - bootstrap_method.bootstrap_arguments = new Array(bootstrap_method.num_bootstrap_arguments); + bootstrap_method_ref: buf.readUint16(), + num_bootstrap_arguments: buf.readUint16() + } + bootstrap_method.bootstrap_arguments = new Array(bootstrap_method.num_bootstrap_arguments) for ( let bootstrapArgumentIndex = 0; bootstrapArgumentIndex < bootstrap_method.num_bootstrap_arguments; bootstrapArgumentIndex++ ) { - bootstrap_method.bootstrap_arguments[bootstrapArgumentIndex] = this.buf.readUint16(); + bootstrap_method.bootstrap_arguments[bootstrapArgumentIndex] = buf.readUint16() } - attribute.bootstrap_methods[bootstrapMethodIndex] = bootstrap_method; + attribute.bootstrap_methods[bootstrapMethodIndex] = bootstrap_method } - break; + break } - case "RuntimeInvisibleTypeAnnotations": - case "RuntimeVisibleTypeAnnotations": { - attribute.num_annotations = this.buf.readUint16(); - attribute.annotations = new Array(attribute.num_annotations); + case 'RuntimeInvisibleTypeAnnotations': + case 'RuntimeVisibleTypeAnnotations': { + attribute.num_annotations = buf.readUint16() + attribute.annotations = new Array(attribute.num_annotations) for (let i = 0; i < attribute.num_annotations; i++) { - attribute.annotations[i] = this._readTypeAnnotation(); + attribute.annotations[i] = this._readTypeAnnotation(buf) } - break; + break } - case "SourceDebugExtension": { - attribute.debug_extension = new Array(attribute.attribute_length); + case 'SourceDebugExtension': { + attribute.debug_extension = new Array(attribute.attribute_length) for (let i = 0; i < attribute.attribute_length; i++) { - attribute.debug_extension[i] = this.buf.readUint8(); + attribute.debug_extension[i] = buf.readUint8() } - break; + break } - case "SourceFile": - attribute.sourcefile_index = this.buf.readUint16(); - break; + case 'SourceFile': + attribute.sourcefile_index = buf.readUint16() + break - case "EnclosingMethod": - attribute.class_index = this.buf.readUint16(); - attribute.method_index = this.buf.readUint16(); - break; + case 'EnclosingMethod': + attribute.class_index = buf.readUint16() + attribute.method_index = buf.readUint16() + break - case "AnnotationDefault": - attribute.default_value = this._readElementValue(); - break; + case 'AnnotationDefault': + attribute.default_value = this._readElementValue(buf) + break - case "MethodParameters": { - attribute.parameters_count = this.buf.readUint8(); - attribute.parameters = new Array(attribute.parameters_count); + case 'MethodParameters': { + attribute.parameters_count = buf.readUint8() + attribute.parameters = new Array(attribute.parameters_count) for (let i = 0; i < attribute.parameters_count; i++) { attribute.parameters[i] = { - name_index: this.buf.readUint16(), - access_flags: this.buf.readUint16(), - }; + name_index: buf.readUint16(), + access_flags: buf.readUint16() + } } - break; - } - - case "ConstantValue": - attribute.constantvalue_index = this.buf.readUint16(); - break; - - case "Signature": - attribute.signature_index = this.buf.readUint16(); - break; - - case "StackMap": - return this._readStackMapAttribute(attribute); - case "Exceptions": - return this._readExceptionsAttribute(attribute); - case "StackMapTable": - return this._readStackMapTableAttribute(attribute); - case "Code": - return this._readCodeAttribute(attribute); - case "LineNumberTable": - return this._readLineNumberTableAttribute(attribute); - case "Module": - return this._readModuleAttribute(attribute); - case "ModulePackages": - return this._readModulePackagesAttribute(attribute); - - case "ModuleMainClass": - attribute.main_class_index = this.buf.readUint16(); - break; - - case "NestHost": - attribute.host_class_index = this.buf.readUint16(); - break; - - case "NestMembers": - attribute.number_of_classes = this.buf.readUint16(); - attribute.classes = new Array(attribute.number_of_classes); + break + } + + case 'ConstantValue': + attribute.constantvalue_index = buf.readUint16() + break + + case 'Signature': + attribute.signature_index = buf.readUint16() + break + + case 'StackMap': + /* c8 ignore next */ + throw new Error('StackMap attribute is not supported') + case 'Exceptions': + return this._readExceptionsAttribute(buf, attribute) + case 'StackMapTable': + return this._readStackMapTableAttribute(buf, attribute) + case 'Code': + return this._readCodeAttribute(buf, attribute, constantPool) + case 'LineNumberTable': + return this._readLineNumberTableAttribute(buf, attribute) + case 'Module': + return this._readModuleAttribute(buf, attribute) + case 'ModulePackages': + return this._readModulePackagesAttribute(buf, attribute) + + case 'ModuleMainClass': + attribute.main_class_index = buf.readUint16() + break + + case 'NestHost': + attribute.host_class_index = buf.readUint16() + break + + case 'NestMembers': + attribute.number_of_classes = buf.readUint16() + attribute.classes = new Array(attribute.number_of_classes) for (let i = 0; i < attribute.number_of_classes; i++) { - attribute.classes[i] = this.buf.readUint16(); + attribute.classes[i] = buf.readUint16() } - break; + break // Unknown attributes // See: https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.1 default: { - attribute.info = new Array(attribute.attribute_length); + attribute.info = new Array(attribute.attribute_length) for (let i = 0; i < attribute.attribute_length; i++) { - attribute.info[i] = this.buf.readUint8(); + attribute.info[i] = buf.readUint8() } } } - return attribute; + return attribute } - _readCodeAttribute(attribute) { - attribute.max_stack = this.buf.readUint16(); - attribute.max_locals = this.buf.readUint16(); - attribute.code_length = this.buf.readUint32(); - attribute.code = new Array(attribute.code_length); + static _readCodeAttribute(buf: ByteBuffer, attribute, constantPool: any[]) { + attribute.max_stack = buf.readUint16() + attribute.max_locals = buf.readUint16() + attribute.code_length = buf.readUint32() + attribute.code = new Array(attribute.code_length) // Reads "code" array for (let i = 0; i < attribute.code_length; i++) { - attribute.code[i] = this.buf.readUint8(); + attribute.code[i] = buf.readUint8() } - attribute.exception_table_length = this.buf.readUint16(); - attribute.exception_table = new Array(attribute.exception_table_length); + attribute.exception_table_length = buf.readUint16() + attribute.exception_table = new Array(attribute.exception_table_length) // Reads exception_table for (let i = 0; i < attribute.exception_table_length; i++) { attribute.exception_table[i] = { - start_pc: this.buf.readUint16(), - end_pc: this.buf.readUint16(), - handler_pc: this.buf.readUint16(), - catch_type: this.buf.readUint16(), - }; + start_pc: buf.readUint16(), + end_pc: buf.readUint16(), + handler_pc: buf.readUint16(), + catch_type: buf.readUint16() + } } - attribute.attributes_count = this.buf.readUint16(); - attribute.attributes = this._readAttributeInfoArray(attribute.attributes_count); - return attribute; + attribute.attributes_count = buf.readUint16() + attribute.attributes = this._readAttributeInfoArray(buf, attribute.attributes_count, constantPool) + return attribute } - _readLineNumberTableAttribute(attribute) { - attribute.line_number_table_length = this.buf.readUint16(); - attribute.line_number_table = new Array(attribute.line_number_table_length); + static _readLineNumberTableAttribute(buf: ByteBuffer, attribute) { + attribute.line_number_table_length = buf.readUint16() + attribute.line_number_table = new Array(attribute.line_number_table_length) for (let i = 0; i < attribute.line_number_table_length; i++) { attribute.line_number_table[i] = { - start_pc: this.buf.readUint16(), - line_number: this.buf.readUint16(), - }; + start_pc: buf.readUint16(), + line_number: buf.readUint16() + } } - return attribute; + return attribute } // TODO: this function is being deoptimized one time... Check why - _readStackMapTableAttribute(attribute) { - attribute.number_of_entries = this.buf.readUint16(); - attribute.entries = new Array(attribute.number_of_entries); + static _readStackMapTableAttribute(buf: ByteBuffer, attribute) { + attribute.number_of_entries = buf.readUint16() + attribute.entries = new Array(attribute.number_of_entries) for (let entryIndex = 0; entryIndex < attribute.number_of_entries; entryIndex++) { const stack_map_frame: any = { - frame_type: this.buf.readUint8(), - }; + frame_type: buf.readUint8() + } // Shorthand - const frame_type = stack_map_frame.frame_type; + const frame_type = stack_map_frame.frame_type /** * offset_delta's that are "constant" are omitted. @@ -524,213 +523,178 @@ export class ClassFile { // SAME if (frame_type >= 0 && frame_type <= 63) { - attribute.entries[entryIndex] = stack_map_frame; - continue; + attribute.entries[entryIndex] = stack_map_frame + continue } // SAME_LOCALS_1_STACK_ITEM if (frame_type >= 64 && frame_type <= 127) { - stack_map_frame.stack = [this._readVerificationTypeInfo()]; + stack_map_frame.stack = [this._readVerificationTypeInfo(buf)] } // SAME_LOCALS_1_STACK_ITEM_EXTENDED else if (stack_map_frame.frame_type === 247) { - stack_map_frame.offset_delta = this.buf.readUint16(); - stack_map_frame.stack = [this._readVerificationTypeInfo()]; + stack_map_frame.offset_delta = buf.readUint16() + stack_map_frame.stack = [this._readVerificationTypeInfo(buf)] } // CHOP = 248-250, SAME_FRAME_EXTENDED = 251 else if (frame_type >= 248 && frame_type <= 251) { - stack_map_frame.offset_delta = this.buf.readUint16(); + stack_map_frame.offset_delta = buf.readUint16() } // APPEND else if (frame_type >= 252 && frame_type <= 254) { - const numberOfLocals = frame_type - 251; + const numberOfLocals = frame_type - 251 - stack_map_frame.offset_delta = this.buf.readUint16(); - stack_map_frame.locals = new Array(numberOfLocals); + stack_map_frame.offset_delta = buf.readUint16() + stack_map_frame.locals = new Array(numberOfLocals) for (let i = 0; i < numberOfLocals; i++) { - stack_map_frame.locals[i] = this._readVerificationTypeInfo(); + stack_map_frame.locals[i] = this._readVerificationTypeInfo(buf) } } // FULL_FRAME else if (frame_type === 255) { - stack_map_frame.offset_delta = this.buf.readUint16(); - stack_map_frame.number_of_locals = this.buf.readUint16(); - stack_map_frame.locals = new Array(stack_map_frame.number_of_locals); + stack_map_frame.offset_delta = buf.readUint16() + stack_map_frame.number_of_locals = buf.readUint16() + stack_map_frame.locals = new Array(stack_map_frame.number_of_locals) for (let i = 0; i < stack_map_frame.number_of_locals; i++) { - stack_map_frame.locals[i] = this._readVerificationTypeInfo(); + stack_map_frame.locals[i] = this._readVerificationTypeInfo(buf) } - stack_map_frame.number_of_stack_items = this.buf.readUint16(); - stack_map_frame.stack = new Array(stack_map_frame.number_of_stack_items); + stack_map_frame.number_of_stack_items = buf.readUint16() + stack_map_frame.stack = new Array(stack_map_frame.number_of_stack_items) for (let i = 0; i < stack_map_frame.number_of_stack_items; i++) { - stack_map_frame.stack[i] = this._readVerificationTypeInfo(); + stack_map_frame.stack[i] = this._readVerificationTypeInfo(buf) } } - attribute.entries[entryIndex] = stack_map_frame; + attribute.entries[entryIndex] = stack_map_frame } - return attribute; + return attribute } - _readExceptionsAttribute(attribute) { - attribute.number_of_exceptions = this.buf.readUint16(); - attribute.exception_index_table = new Array(attribute.number_of_exceptions); + static _readExceptionsAttribute(buf: ByteBuffer, attribute) { + attribute.number_of_exceptions = buf.readUint16() + attribute.exception_index_table = new Array(attribute.number_of_exceptions) for (let i = 0; i < attribute.number_of_exceptions; i++) { - attribute.exception_index_table[i] = this.buf.readUint16(); + attribute.exception_index_table[i] = buf.readUint16() } - return attribute; + return attribute } - /** - * http://download.oracle.com/otndocs/jcp/7247-j2me_cldc-1.1-fr-spec-oth-JSpec/ - * - * Appendix1-verifier.pdf at "2.1 Stack map format" - * - * "According to the CLDC specification, the sizes of some fields are not 16bit - * but 32bit if the code size is more than 64K or the number of the local variables - * is more than 64K. However, for the J2ME CLDC technology, they are always 16bit. - * The implementation of the StackMap class assumes they are 16bit." - javaassist - */ - _readStackMapAttribute(attribute) { - attribute.number_of_entries = this.buf.readUint16(); - attribute.entries = new Array(attribute.number_of_entries); - - for (let entryIndex = 0; entryIndex < attribute.number_of_entries; entryIndex++) { - const stack_map_frame: any = { offset: this.buf.readUint16() }; - - // Read locals - stack_map_frame.number_of_locals = this.buf.readUint16(); - stack_map_frame.locals = new Array(stack_map_frame.number_of_locals); - for (let i = 0; i < stack_map_frame.number_of_locals; i++) { - stack_map_frame.locals[i] = this._readVerificationTypeInfo(); - } - - // Read stack - stack_map_frame.number_of_stack_items = this.buf.readUint16(); - stack_map_frame.stack = new Array(stack_map_frame.number_of_stack_items); - for (let i = 0; i < stack_map_frame.number_of_stack_items; i++) { - stack_map_frame.stack[i] = this._readVerificationTypeInfo(); - } - attribute.entries[entryIndex] = stack_map_frame; - } - return attribute; - } - - _readModuleAttribute(attribute) { - attribute.module_name_index = this.buf.readUint16(); - attribute.module_flags = this.buf.readUint16(); - attribute.module_version_index = this.buf.readUint16(); - attribute.requires_count = this.buf.readUint16(); - attribute.requires = new Array(attribute.requires_count); + static _readModuleAttribute(buf: ByteBuffer, attribute) { + attribute.module_name_index = buf.readUint16() + attribute.module_flags = buf.readUint16() + attribute.module_version_index = buf.readUint16() + attribute.requires_count = buf.readUint16() + attribute.requires = new Array(attribute.requires_count) for (let i = 0; i < attribute.requires_count; i++) { attribute.requires[i] = { - requires_index: this.buf.readUint16(), - requires_flags: this.buf.readUint16(), - requires_version_index: this.buf.readUint16(), - }; + requires_index: buf.readUint16(), + requires_flags: buf.readUint16(), + requires_version_index: buf.readUint16() + } } - attribute.exports_count = this.buf.readUint16(); - attribute.exports = new Array(attribute.exports_count); + attribute.exports_count = buf.readUint16() + attribute.exports = new Array(attribute.exports_count) for (let exportIndex = 0; exportIndex < attribute.exports_count; exportIndex++) { const exportEntry: any = { - exports_index: this.buf.readUint16(), - exports_flags: this.buf.readUint16(), - exports_to_count: this.buf.readUint16(), - }; - exportEntry.exports_to_index = new Array(exportEntry.exports_to_count); + exports_index: buf.readUint16(), + exports_flags: buf.readUint16(), + exports_to_count: buf.readUint16() + } + exportEntry.exports_to_index = new Array(exportEntry.exports_to_count) for (let exportsToIndex = 0; exportsToIndex < exportEntry.exports_to_count; exportsToIndex++) { - exportEntry.exports_to_index[exportsToIndex] = this.buf.readUint16(); + exportEntry.exports_to_index[exportsToIndex] = buf.readUint16() } - attribute.exports[exportIndex] = exportEntry; + attribute.exports[exportIndex] = exportEntry } - attribute.opens_count = this.buf.readUint16(); - attribute.opens = new Array(attribute.opens_count); + attribute.opens_count = buf.readUint16() + attribute.opens = new Array(attribute.opens_count) for (let openIndex = 0; openIndex < attribute.opens_count; openIndex++) { const openEntry: any = { - opens_index: this.buf.readUint16(), - opens_flags: this.buf.readUint16(), - opens_to_count: this.buf.readUint16(), - }; - openEntry.opens_to_index = new Array(openEntry.opens_to_count); + opens_index: buf.readUint16(), + opens_flags: buf.readUint16(), + opens_to_count: buf.readUint16() + } + openEntry.opens_to_index = new Array(openEntry.opens_to_count) for (let opensToIndex = 0; opensToIndex < openEntry.opens_to_count; opensToIndex++) { - openEntry.opens_to_index[opensToIndex] = this.buf.readUint16(); + openEntry.opens_to_index[opensToIndex] = buf.readUint16() } - attribute.opens[openIndex] = openEntry; + attribute.opens[openIndex] = openEntry } - attribute.uses_count = this.buf.readUint16(); - attribute.uses_index = new Array(attribute.uses_count); + attribute.uses_count = buf.readUint16() + attribute.uses_index = new Array(attribute.uses_count) for (let i = 0; i < attribute.uses_count; i++) { - attribute.uses_index[i] = this.buf.readUint16(); + attribute.uses_index[i] = buf.readUint16() } - attribute.provides_count = this.buf.readUint16(); - attribute.provides = new Array(attribute.provides_count); + attribute.provides_count = buf.readUint16() + attribute.provides = new Array(attribute.provides_count) for (let providesIndex = 0; providesIndex < attribute.provides_count; providesIndex++) { const provideEntry: any = { - provides_index: this.buf.readUint16(), - provides_with_count: this.buf.readUint16(), - }; - provideEntry.provides_with_index = new Array(provideEntry.provides_with_count); + provides_index: buf.readUint16(), + provides_with_count: buf.readUint16() + } + provideEntry.provides_with_index = new Array(provideEntry.provides_with_count) for (let providesWithIndex = 0; providesWithIndex < provideEntry.provides_with_count; providesWithIndex++) { - provideEntry.provides_with_index[providesWithIndex] = this.buf.readUint16(); + provideEntry.provides_with_index[providesWithIndex] = buf.readUint16() } - attribute.provides[providesIndex] = provideEntry; + attribute.provides[providesIndex] = provideEntry } - return attribute; + return attribute } - _readModulePackagesAttribute(attribute) { - attribute.package_count = this.buf.readUint16(); - attribute.package_index = new Array(attribute.package_count); + static _readModulePackagesAttribute(buf: ByteBuffer, attribute) { + attribute.package_count = buf.readUint16() + attribute.package_index = new Array(attribute.package_count) for (let i = 0; i < attribute.package_count; i++) { - attribute.package_index[i] = this.buf.readUint16(); + attribute.package_index[i] = buf.readUint16() } - return attribute; + return attribute } /** * Reads the "annotation" structure * https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.16 */ - _readAttributeAnnotation() { + static _readAttributeAnnotation(buf: ByteBuffer) { const annotation: any = { - type_index: this.buf.readUint16(), - num_element_value_pairs: this.buf.readUint16(), - }; - annotation.element_value_pairs = new Array(annotation.num_element_value_pairs); + type_index: buf.readUint16(), + num_element_value_pairs: buf.readUint16() + } + annotation.element_value_pairs = new Array(annotation.num_element_value_pairs) for (let i = 0; i < annotation.num_element_value_pairs; i++) { annotation.element_value_pairs[i] = { - element_name_index: this.buf.readUint16(), - element_value: this._readElementValue(), - }; + element_name_index: buf.readUint16(), + element_value: this._readElementValue(buf) + } } - return annotation; + return annotation } /** * Reads the "element_value" structure * https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.16.1 */ - _readElementValue() { + static _readElementValue(buf: ByteBuffer) { const element_value: any = { - tag: this.buf.readUint8(), - value: {}, - }; + tag: buf.readUint8(), + value: {} + } /** * https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.16.1-130 @@ -738,34 +702,34 @@ export class ClassFile { switch (element_value.tag) { case 101: // e element_value.value.enum_const_value = { - type_name_index: this.buf.readUint16(), - const_name_index: this.buf.readUint16(), - }; - break; + type_name_index: buf.readUint16(), + const_name_index: buf.readUint16() + } + break case 99: // c - element_value.value.class_info_index = this.buf.readUint16(); - break; + element_value.value.class_info_index = buf.readUint16() + break case 91: { // [ - const num_values = this.buf.readUint16(); - const values = new Array(num_values); + const num_values = buf.readUint16() + const values = new Array(num_values) for (let i = 0; i < num_values; i++) { - values[i] = this._readElementValue(); + values[i] = this._readElementValue(buf) } element_value.value.array_value = { num_values, - values, - }; - break; + values + } + break } case 64: // @ - element_value.value.annotation = this._readAttributeAnnotation(); - break; + element_value.value.annotation = this._readAttributeAnnotation(buf) + break case 66: // B case 67: // C @@ -776,37 +740,36 @@ export class ClassFile { case 83: // S case 90: // Z case 115: // s - element_value.value.const_value_index = this.buf.readUint16(); - break; + element_value.value.const_value_index = buf.readUint16() + break default: - throw Error(`Unexpected tag: ${element_value.tag}`); + throw Error(`Unexpected tag: ${element_value.tag}`) } - return element_value; + return element_value } - _readInterfaces(interfaceCount) { - const interfaces = new Array(interfaceCount); + static _readInterfaces(buf: ByteBuffer, interfaceCount: number) { + const interfaces = new Array(interfaceCount) for (let i = 0; i < interfaceCount; i++) { - interfaces[i] = this.buf.readUint16(); + interfaces[i] = buf.readUint16() } - return interfaces; + return interfaces } /** * https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4 */ - _readConstantPool(poolCount) { + static _readConstantPool(buf: ByteBuffer, poolCount: number) { /** * https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.1 * The constant_pool table is indexed from 1 to constant_pool_count-1. */ - const pool = new Array(poolCount); - + const pool = new Array(poolCount) for (let i = 1; i <= poolCount; i++) { - const entry = this._readConstantPoolEntry(); - pool[i] = entry; + const entry = this._readConstantPoolEntry(buf) + pool[i] = entry /** * https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4.5 @@ -817,45 +780,45 @@ export class ClassFile { * The constant_pool index n+1 must be valid but is considered unusable. */ if (entry.tag === ConstantType.LONG || entry.tag === ConstantType.DOUBLE) { - pool[++i] = undefined; + pool[++i] = undefined } } - return pool; + return pool } - _readUtf8PoolEntry(tag) { - const length = this.buf.readUint16(); - const bytes = new Array(length); + static _readUtf8PoolEntry(buf: ByteBuffer, tag: number) { + const length = buf.readUint16() + const bytes = new Array(length) for (let i = 0; i < length; i++) { - bytes[i] = this.buf.readUint8(); + bytes[i] = buf.readUint8() } - return { tag, length, bytes }; + return { tag, length, bytes } } - _readConstantPoolEntry() { - const tag = this.buf.readUint8(); + static _readConstantPoolEntry(buf: ByteBuffer) { + const tag = buf.readUint8() switch (tag) { case ConstantType.UTF8: - return this._readUtf8PoolEntry(tag); + return this._readUtf8PoolEntry(buf, tag) case ConstantType.INTEGER: case ConstantType.FLOAT: - return { tag, bytes: this.buf.readUint32() }; + return { tag, bytes: buf.readUint32() } case ConstantType.LONG: case ConstantType.DOUBLE: - return { tag, high_bytes: this.buf.readUint32(), low_bytes: this.buf.readUint32() }; + return { tag, high_bytes: buf.readUint32(), low_bytes: buf.readUint32() } case ConstantType.PACKAGE: case ConstantType.MODULE: case ConstantType.CLASS: - return { tag, name_index: this.buf.readUint16() }; + return { tag, name_index: buf.readUint16() } case ConstantType.STRING: - return { tag, string_index: this.buf.readUint16() }; + return { tag, string_index: buf.readUint16() } /** * Fields, methods, and interface methods are represented by similar structures @@ -864,23 +827,23 @@ export class ClassFile { case ConstantType.FIELDREF: case ConstantType.METHODREF: case ConstantType.INTERFACE_METHODREF: - return { tag, class_index: this.buf.readUint16(), name_and_type_index: this.buf.readUint16() }; + return { tag, class_index: buf.readUint16(), name_and_type_index: buf.readUint16() } case ConstantType.NAME_AND_TYPE: - return { tag, name_index: this.buf.readUint16(), descriptor_index: this.buf.readUint16() }; + return { tag, name_index: buf.readUint16(), descriptor_index: buf.readUint16() } case ConstantType.METHOD_HANDLE: - return { tag, reference_kind: this.buf.readUint8(), reference_index: this.buf.readUint16() }; + return { tag, reference_kind: buf.readUint8(), reference_index: buf.readUint16() } case ConstantType.METHOD_TYPE: - return { tag, descriptor_index: this.buf.readUint16() }; + return { tag, descriptor_index: buf.readUint16() } case ConstantType.DYNAMIC: case ConstantType.INVOKE_DYNAMIC: - return { tag, bootstrap_method_attr_index: this.buf.readUint16(), name_and_type_index: this.buf.readUint16() }; + return { tag, bootstrap_method_attr_index: buf.readUint16(), name_and_type_index: buf.readUint16() } default: - throw Error(`Unexpected tag: ${tag}`); + throw Error(`Unexpected tag: ${tag}`) } } } @@ -899,8 +862,8 @@ export class JavaClassFileReader { * @see {@link https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.1} */ static readData(source: Uint8Array | Buffer | ArrayBuffer): ClassFile { - return ClassFile.fromData(ByteBuffer.wrap(source)); + return ClassFile.fromData(ByteBuffer.wrap(source)) } } -export default JavaClassFileReader; +export default JavaClassFileReader diff --git a/packages/java/javaclasses/java-class-types.ts b/packages/java/javaclasses/java-class-types.ts new file mode 100644 index 00000000..b0622d6c --- /dev/null +++ b/packages/java/javaclasses/java-class-types.ts @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2024 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under the License. + */ + +export type MemberInfo = { + access_flags: number + name_index: number + descriptor_index: number + attributes_count: number + attributes: any[] +} + +export type TypeInfo = { + tag: number + cpool_index?: number + offset?: number +} + +export type ConstantInfo = { + tag: number + length: number + bytes: number[] +} + +export type AttributeInfoBase = { + attribute_name_index: number + attribute_length: number +} + +export type AttributeInfo = AttributeInfoBase & { + [key: string]: any +} + +// Class info record shape. +export type ClassInfo = { + tag: 7 + name_index: number +} + +// Generic attributes. +export type GenericInfo = { + [key: string]: any +} + +// Java Module `requires` block (low-level). +export type ModuleRequiresInfo = GenericInfo & { + requires_index: number + requires_flags: number + requires_version_index: number +} + +// Java Module `exports` block (low-level). +export type ModuleExportsInfo = GenericInfo & { + exports_index: number + exports_flags: number + exports_to_count: number + exports_to_index: number[] +} + +// Java Module `opens` block (low-level). +export type ModuleOpensInfo = GenericInfo & { + opens_index: number + opens_flags: number + opens_to_count: number + opens_to_index: number[] +} + +// Java Module `uses` block (low-level). +export type ModuleUsesInfo = number + +// Java Module `provides` block (low-level). +export type ModuleProvidesInfo = GenericInfo & { + provides_index: number + provides_with_count: number + provides_with_index: number[] +} + +// Java Module attributes. +export type ModuleInfoAttributes = AttributeInfo & { + module_name_index: number + module_flags: number + module_version_index: number + requires_count: number + requires: ModuleRequiresInfo[] + exports_count: number + exports: ModuleExportsInfo[] + opens_count: number + opens: ModuleOpensInfo[] + uses_count: number + uses_index: ModuleUsesInfo[] + provides_count: number + provides: ModuleProvidesInfo[] +} diff --git a/packages/java/javaclasses/modifier.ts b/packages/java/javaclasses/modifier.ts index db0c1c69..1f336531 100644 --- a/packages/java/javaclasses/modifier.ts +++ b/packages/java/javaclasses/modifier.ts @@ -46,7 +46,7 @@ export enum Modifier { ENUM = 0x4000, // Java SE 9 - MODULE = 0x8000, + MODULE = 0x8000 } -export default Modifier; +export default Modifier diff --git a/packages/java/javaclasses/opcode.ts b/packages/java/javaclasses/opcode.ts index ba709682..0a887704 100644 --- a/packages/java/javaclasses/opcode.ts +++ b/packages/java/javaclasses/opcode.ts @@ -228,7 +228,7 @@ export enum Opcode { JSR_W = 0xc9, BREAKPOINT = 0xca, IMPDEP1 = 0xfe, - IMPDEP2 = 0xff, + IMPDEP2 = 0xff } -export default Opcode; +export default Opcode diff --git a/packages/java/javamodules/jdk-modules.ts b/packages/java/javamodules/jdk-modules.ts new file mode 100644 index 00000000..07a503f8 --- /dev/null +++ b/packages/java/javamodules/jdk-modules.ts @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2024 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under the License. + */ + +/** + * Built-in Java Modules + * + * Modules within the `java.*` namespace are "built-in" to the language. + */ +export enum BuiltinModule { + JAVA_BASE = 'java.base', + JAVA_COMPILER = 'java.compiler', + JAVA_DATATRANSFER = 'java.datatransfer', + JAVA_DESKTOP = 'java.desktop', + JAVA_INSTRUMENT = 'java.instrument', + JAVA_LOGGING = 'java.logging', + JAVA_MANAGEMENT = 'java.management', + JAVA_MANAGEMENT_RMI = 'java.management.rmi', + JAVA_NAMING = 'java.naming', + JAVA_NET_HTTP = 'java.net.http', + JAVA_PREFS = 'java.prefs', + JAVA_RMI = 'java.rmi', + JAVA_SCRIPTING = 'java.scripting', + JAVA_SE = 'java.se', + JAVA_SECURITY_JGSS = 'java.security.jgss', + JAVA_SECURITY_SASL = 'java.security.sasl', + JAVA_SMARTCARDIO = 'java.smartcardio', + JAVA_SQL = 'java.sql', + JAVA_SQL_ROWSET = 'java.sql.rowset', + JAVA_TRANSACTION_XA = 'java.transaction.xa', + JAVA_XML = 'java.xml', + JAVA_XML_CRYPTO = 'java.xml.crypto' +} + +/** + * JDK Modules + * + * Modules within the `jdk.*` namespace are JDK-only. + */ +export enum JdkModule { + JDK_ACCESSIBILITY = 'jdk.accessibility', + JDK_ATTACH = 'jdk.attach', + JDK_CHARSETS = 'jdk.charsets', + JDK_COMPILER = 'jdk.compiler', + JDK_CRYPTO_CRYPTOKI = 'jdk.crypto.cryptoki', + JDK_CRYPTO_EC = 'jdk.crypto.ec', + JDK_DYNALINK = 'jdk.dynalink', + JDK_EDITPAD = 'jdk.editpad', + JDK_HOTSPOT_AGENT = 'jdk.hotspot.agent', + JDK_HTTPSERVER = 'jdk.httpserver', + JDK_INCUBATOR_VECTOR = 'jdk.incubator.vector', + JDK_JARTOOL = 'jdk.jartool', + JDK_JAVADOC = 'jdk.javadoc', + JDK_JCMD = 'jdk.jcmd', + JDK_JCONSOLE = 'jdk.jconsole', + JDK_JDEPS = 'jdk.jdeps', + JDK_JDI = 'jdk.jdi', + JDK_JDWP_AGENT = 'jdk.jdwp.agent', + JDK_JFR = 'jdk.jfr', + JDK_JLINK = 'jdk.jlink', + JDK_JPACKAGE = 'jdk.jpackage', + JDK_JSHELL = 'jdk.jshell', + JDK_JSOBJECT = 'jdk.jsobject', + JDK_JSTATD = 'jdk.jstatd', + JDK_LOCALEDATA = 'jdk.localedata', + JDK_MANAGEMENT = 'jdk.management', + JDK_MANAGEMENT_AGENT = 'jdk.management.agent', + JDK_MANAGEMENT_JFR = 'jdk.management.jfr', + JDK_NAMING_DNS = 'jdk.naming.dns', + JDK_NAMING_RMI = 'jdk.naming.rmi', + JDK_NET = 'jdk.net', + JDK_NIO_MAPMODE = 'jdk.nio.mapmode', + JDK_SCTP = 'jdk.sctp', + JDK_SECURITY_AUTH = 'jdk.security.auth', + JDK_SECURITY_JGSS = 'jdk.security.jgss', + JDK_XML_DOM = 'jdk.xml.dom', + JDK_ZIPFS = 'jdk.zipfs', + JDK_UNSUPPORTED = 'jdk.unsupported' +} diff --git a/packages/java/javamodules/module-flags.ts b/packages/java/javamodules/module-flags.ts new file mode 100644 index 00000000..d4fd10b0 --- /dev/null +++ b/packages/java/javamodules/module-flags.ts @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under the License. + */ + +/** + * Module Flags: Base Values + * + * See: https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-4.html#jvms-4.7.25 + */ +export const ModularFlags = { + SYNTHETIC: 0x1000, + MANDATED: 0x8000 +} + +/** + * Module Flags: Modules + * + * See: https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-4.html#jvms-4.7.25 + */ +export const ModuleFlags = { + ...ModularFlags, + OPEN: 0x0020 +} + +/** + * Module Flags: Requires Statements + * + * See: https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-4.html#jvms-4.7.25 + */ +export const RequiresFlags = { + ...ModularFlags, + TRANSITIVE: 0x0020, + STATIC: 0x0040 +} + +/** + * Module Flags: Exports Statements + * + * See: https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-4.html#jvms-4.7.25 + */ +export const ExportsFlags = ModularFlags + +/** + * Module Flags: Opens Statements + * + * See: https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-4.html#jvms-4.7.25 + */ +export const OpensFlags = ModularFlags diff --git a/packages/java/package.json b/packages/java/package.json index 1dc946e3..fe1f3b95 100644 --- a/packages/java/package.json +++ b/packages/java/package.json @@ -1,46 +1,28 @@ { "name": "@javamodules/java", - "version": "1.0.1", - "type": "module", - "main": "dist/index.mjs", - "license": "Apache-2.0", + "version": "1.0.2", "description": "Tools for working with Java class files, Java toolchains, and for compiling Java.", - "homepage": "https://github.com/javamodules", "keywords": [ "java", "jvm", "tools", "build-tools" ], - "files": [ - "dist/**", - "!dist/*test*", - "!dist/tests" - ], - "publishConfig": { - "provenance": true, - "access": "public" + "homepage": "https://github.com/javamodules", + "bugs": { + "url": "https://github.com/elide-dev/jpms/issues" }, "repository": { "type": "git", "url": "https://github.com/elide-dev/jpms", "directory": "packages/java" }, - "bugs": { - "url": "https://github.com/elide-dev/jpms/issues" - }, + "license": "Apache-2.0", "author": { "name": "Sam Gammon", "url": "https://github.com/sgammon" }, - "scripts": { - "test:bun": "bun test", - "test:node": "node --experimental-vm-modules node_modules/jest/bin/jest.js", - "publish:dry": "npm publish --no-git-checks --dry-run", - "publish:live": "npm publish --no-git-checks", - "pack": "npm pack", - "build": "tsc -p ." - }, + "type": "module", "imports": { "#tests": { "bun": "bun:test", @@ -57,31 +39,29 @@ "types": "./dist/index.d.ts" } }, - "dependencies": { - "@endo/zip": "1.0.2", - "bytebuffer": "5.0.1", - "glob": "10.3.10", - "semver": "7.6.0" - }, - "devDependencies": { - "@jest/globals": "29.7.0", - "@types/bytebuffer": "5.0.48", - "@types/jest": "29.5.12", - "@types/node": "20.11.29", - "jest": "29.7.0", - "jest-junit": "16.0.0", - "ts-jest": "29.1.2", - "typescript": "5.4.2" + "main": "dist/index.mjs", + "files": [ + "dist/**", + "!dist/*test*", + "!dist/tests" + ], + "scripts": { + "build": "tsc -p .", + "pack": "npm pack", + "publish:dry": "npm publish --no-git-checks --dry-run", + "publish:live": "npm publish --no-git-checks", + "test:bun": "bun test", + "test:node": "node --experimental-vm-modules node_modules/jest/bin/jest.js" }, "jest": { - "preset": "ts-jest", "collectCoverage": true, - "coverageProvider": "v8", "coverageDirectory": "reports", + "coverageProvider": "v8", "coverageReporters": [ "lcov", "text-summary" ], + "preset": "ts-jest", "reporters": [ "default", "github-actions", @@ -96,5 +76,26 @@ "testMatch": [ "/tests/*.test.ts" ] + }, + "dependencies": { + "bytebuffer": "5.0.1", + "fflate": "0.8.2", + "glob": "10.3.10", + "memfs": "4.8.0", + "semver": "7.6.0" + }, + "devDependencies": { + "@jest/globals": "29.7.0", + "@types/bytebuffer": "5.0.48", + "@types/jest": "29.5.12", + "@types/node": "20.11.29", + "jest": "29.7.0", + "jest-junit": "16.0.0", + "ts-jest": "29.1.2", + "typescript": "5.4.2" + }, + "publishConfig": { + "access": "public", + "provenance": true } } diff --git a/packages/java/tests/classfile-parser.test.ts b/packages/java/tests/classfile-parser.test.ts deleted file mode 100644 index 713bbeee..00000000 --- a/packages/java/tests/classfile-parser.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2024 Elide Technologies, Inc. - * - * Licensed under the MIT license (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://opensource.org/license/mit/ - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under the License. - */ - -import { expect, test } from "@jest/globals"; -import { JavaClassFile } from "../classfile-parser"; - -test("parse a basic java class", () => { - // - expect(5).toBe(5); - expect(JavaClassFile).toBeDefined(); -}); diff --git a/packages/java/tests/compiler/Hello.java b/packages/java/tests/compiler/Hello.java new file mode 100644 index 00000000..9b16c81a --- /dev/null +++ b/packages/java/tests/compiler/Hello.java @@ -0,0 +1,7 @@ +package hello; + +public class Hello { + public static void main(String[] args) { + System.out.println("Hello, World!"); + } +} diff --git a/packages/java/tests/compiler/complex/Another.java b/packages/java/tests/compiler/complex/Another.java new file mode 100644 index 00000000..7f7bf53b --- /dev/null +++ b/packages/java/tests/compiler/complex/Another.java @@ -0,0 +1,7 @@ +package another; + +public class Another { + public static void main(String[] args) { + System.out.println("Hello again, World!"); + } +} diff --git a/packages/java/tests/compiler/complex/Hello.java b/packages/java/tests/compiler/complex/Hello.java new file mode 100644 index 00000000..9b16c81a --- /dev/null +++ b/packages/java/tests/compiler/complex/Hello.java @@ -0,0 +1,7 @@ +package hello; + +public class Hello { + public static void main(String[] args) { + System.out.println("Hello, World!"); + } +} diff --git a/packages/java/tests/compiler/complex/Implementation.java b/packages/java/tests/compiler/complex/Implementation.java new file mode 100644 index 00000000..ea7ca029 --- /dev/null +++ b/packages/java/tests/compiler/complex/Implementation.java @@ -0,0 +1,5 @@ +package hello; + +public class Implementation implements Service { + // Empty +} diff --git a/packages/java/tests/compiler/complex/META-INF/services/hello.Service b/packages/java/tests/compiler/complex/META-INF/services/hello.Service new file mode 100644 index 00000000..e69de29b diff --git a/packages/java/tests/compiler/complex/Service.java b/packages/java/tests/compiler/complex/Service.java new file mode 100644 index 00000000..dca9a372 --- /dev/null +++ b/packages/java/tests/compiler/complex/Service.java @@ -0,0 +1,5 @@ +package hello; + +public interface Service { + // Empty +} diff --git a/packages/java/tests/compiler/complex/module-info.java b/packages/java/tests/compiler/complex/module-info.java new file mode 100644 index 00000000..0934d8e9 --- /dev/null +++ b/packages/java/tests/compiler/complex/module-info.java @@ -0,0 +1,15 @@ + +module complex { + requires transitive java.compiler; + requires static java.desktop; + requires java.logging; + + exports hello; + exports another to sample; + + uses hello.Service; + provides hello.Service with hello.Implementation; + + opens hello; + opens another to sample; +} diff --git a/packages/java/tests/compiler/module-info.java b/packages/java/tests/compiler/module-info.java new file mode 100644 index 00000000..8965e6dc --- /dev/null +++ b/packages/java/tests/compiler/module-info.java @@ -0,0 +1,5 @@ + +module sample { + requires java.logging; + exports hello; +} diff --git a/packages/java/tests/compiler/open/Hello.java b/packages/java/tests/compiler/open/Hello.java new file mode 100644 index 00000000..9b16c81a --- /dev/null +++ b/packages/java/tests/compiler/open/Hello.java @@ -0,0 +1,7 @@ +package hello; + +public class Hello { + public static void main(String[] args) { + System.out.println("Hello, World!"); + } +} diff --git a/packages/java/tests/compiler/open/module-info.java b/packages/java/tests/compiler/open/module-info.java new file mode 100644 index 00000000..7809f48c --- /dev/null +++ b/packages/java/tests/compiler/open/module-info.java @@ -0,0 +1,4 @@ + +open module sample { + // Nothing at this time +} diff --git a/packages/java/tests/java-classfile-parser.test.ts b/packages/java/tests/java-classfile-parser.test.ts new file mode 100644 index 00000000..467d37ea --- /dev/null +++ b/packages/java/tests/java-classfile-parser.test.ts @@ -0,0 +1,929 @@ +/* + * Copyright (c) 2024 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under the License. + */ + +import assert from 'node:assert' +import fs from 'node:fs' +import { join } from 'node:path' +import { createHash } from 'node:crypto' +import ByteBuffer from 'bytebuffer' +import CPUtil from '../javaclasses/constant-pool' +import { describe, expect, it } from '@jest/globals' +import { InstructionParser } from '../javaclasses/instruction-parser' +import Modifier from '../javaclasses/modifier' +import { ClassFile } from '../javaclasses/java-class-reader' +import { ConstantType } from '../javaclasses/constant-type' +import { compileJava } from './testutil.test' +import { JavaToolchain } from '../java-home' +import { JavaClassFile } from '../java-classfile' + +const TMP_DIR_PREFIX = '/tmp/java-classfile-parser-test-' + +describe('instruction parser', () => { + it('fromBytecode() === toBytecode()', async () => { + const code = ` + public class Foo { + + public void foo() { + int val = 23; + + switch (val) { + case 22: + break; + case 21: + break; + default: + break; + } + + int val2 = Integer.MAX_VALUE; + + switch (val2) { + case Integer.MAX_VALUE: + break; + case Integer.MIN_VALUE: + break; + } + + String x = "asdasdsad"; + + switch (x) { + case "asdasdsad": return; + case "asdasdasd": break; + } + } + + } + ` + + const classFile = await compileAndRead('Foo', code) + const method0 = classFile.methods[0] + + const codeAttr = getAttribute(method0, 'Code', classFile) + const originalBytecode = codeAttr.code + + const parsedInstructions = InstructionParser.fromBytecode(originalBytecode) + const rewrittenBytecode = InstructionParser.toBytecode(parsedInstructions) + parsedInstructions.forEach(inst => { + expect(inst.toString()).toBeDefined() + }) + + assert.deepEqual(originalBytecode, rewrittenBytecode) + }) +}) + +describe('ReadMethodsTest_0', () => { + const code = ` + public class ReadMethodsTest_0 { + + @Deprecated + static int x = 1; + + public ReadMethodsTest_0() throws java.io.IOException { + // Used to test verification_type_info + int i = 25; + double j = 76; + ReadMethodsTest_0 xx = null; + + System.out.println("Hello World"); + System.out.println(xx + " " + i + " " + j); + try { + System.out.println(); + } catch (Exception ex) { + ex.printStackTrace(); + } + + try { + System.out.println(); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + public int add(int a, int b) { + return a + b; + } + + } +` + + describe('the "SourceFile" attribute', () => { + it('should exist', async () => { + const classFile = await compileAndRead('ReadMethodsTest_0', code, { + javac_flags: '-parameters' + }) + const srcFileAttr = classFile.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'SourceFile' + })[0] + assert.notEqual(srcFileAttr, undefined) + }) + + it('should be equal to "ReadMethodsTest_0.java"', async () => { + const classFile = await compileAndRead('ReadMethodsTest_0', code, { + javac_flags: '-parameters' + }) + const srcFileAttr = classFile.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'SourceFile' + })[0] + assert.equal(CPUtil.getString(classFile, srcFileAttr.sourcefile_index), 'ReadMethodsTest_0.java') + }) + }) + + describe('the constructor', () => { + it('name should be equal to ""', async () => { + const classFile = await compileAndRead('ReadMethodsTest_0', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[0] + + const name = CPUtil.getString(classFile, method.name_index) + assert.equal(name, '') + }) + + it('descriptor should be equal to "()V"', async () => { + const classFile = await compileAndRead('ReadMethodsTest_0', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[0] + + const descriptor = CPUtil.getString(classFile, method.descriptor_index) + assert.equal(descriptor, '()V') + }) + + describe('the Code attribute', () => { + it('should exist', async () => { + const classFile = await compileAndRead('ReadMethodsTest_0', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[0] + const codeAttr = getAttribute(method, 'Code', classFile) + assert.notEqual(codeAttr, undefined) + }) + + it('the exception_table_length should be equal to 2', async () => { + const classFile = await compileAndRead('ReadMethodsTest_0', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[0] + const codeAttr = getAttribute(method, 'Code', classFile) + const table_len = codeAttr.exception_table_length + assert.equal(table_len, 2) + assert.equal(codeAttr.exception_table.length, table_len) + }) + }) + }) + + describe('the "add" method', () => { + it('name should be equal to "add"', async () => { + const classFile = await compileAndRead('ReadMethodsTest_0', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[1] + const name = CPUtil.getString(classFile, method.name_index) + assert.equal(name, 'add') + }) + + it('descriptor should be equal to "(II)I"', async () => { + const classFile = await compileAndRead('ReadMethodsTest_0', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[1] + const descriptor = CPUtil.getString(classFile, method.descriptor_index) + assert.equal(descriptor, '(II)I') + }) + + describe('the Code attribute', () => { + it('should exist', async () => { + const classFile = await compileAndRead('ReadMethodsTest_0', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[1] + const codeAttr = getAttribute(method, 'Code', classFile) + assert.notEqual(codeAttr, undefined) + }) + + it('the bytecode should be equals to [iload_1, iload_2, iadd, ireturn]', async () => { + const classFile = await compileAndRead('ReadMethodsTest_0', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[1] + const codeAttr = getAttribute(method, 'Code', classFile) + assert.deepEqual(codeAttr.code, [ + 0x1b, // iload_1 + 0x1c, // iload_2 + 0x60, // iadd + 0xac // ireturn + ]) + }) + }) + + describe('the "MethodParameters" attribute', () => { + it('should exist', async () => { + const classFile = await compileAndRead('ReadMethodsTest_0', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[1] + const mpAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'MethodParameters' + })[0] + assert.notEqual(mpAttr, undefined) + }) + + it('parameters_count should be equal to 2', async () => { + const classFile = await compileAndRead('ReadMethodsTest_0', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[1] + const mpAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'MethodParameters' + })[0] + assert.equal(mpAttr.parameters_count, 2) + }) + + it('parameters names should be equal to [a, b]', async () => { + const classFile = await compileAndRead('ReadMethodsTest_0', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[1] + const mpAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'MethodParameters' + })[0] + const params = mpAttr.parameters + assert.equal(CPUtil.getString(classFile, params[0].name_index), 'a') + assert.equal(CPUtil.getString(classFile, params[1].name_index), 'b') + }) + }) + }) +}) + +describe('ReadMethodsTest_1', () => { + const code = ` + import java.lang.annotation.*; + import java.util.*; + + public class ReadMethodsTest_1 { + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE_PARAMETER) + public @interface TypeAnnotation_1 { + int value() default 0; + } + + <@TypeAnnotation_1 T> T generic() { + return null; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE_PARAMETER, ElementType.TYPE_USE, ElementType.TYPE }) + public @interface A { } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE_PARAMETER, ElementType.TYPE_USE, ElementType.TYPE }) + public @interface B { } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE_PARAMETER, ElementType.TYPE_USE, ElementType.TYPE }) + public @interface C { } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE_PARAMETER, ElementType.TYPE_USE, ElementType.TYPE }) + public @interface D { } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE_PARAMETER, ElementType.TYPE_USE, ElementType.TYPE }) + public @interface E { } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE_PARAMETER, ElementType.TYPE_USE, ElementType.TYPE }) + public @interface F { } + + void generic2(@A List<@B Comparable<@F Object @C [] @D [] @E []>> list) { + } + + } +` + + describe('#generic()', () => { + describe('attribute: RuntimeVisibleTypeAnnotations', () => { + it('!== undefined', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[1] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + + assert.notEqual(rvtAttr, undefined) + }) + + it('.num_annotations should be equal to 1', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[1] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + + assert.equal(rvtAttr.num_annotations, 1) + assert.equal(rvtAttr.annotations.length, 1) + }) + + it('.annotations[0] type should be equals to "TypeAnnotation_1"', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[1] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + + const type = CPUtil.getString(classFile, rvtAttr.annotations[0].type_index) + assert.equal(type, 'LReadMethodsTest_1$TypeAnnotation_1;') + }) + }) + }) + + /** + * https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.20.2 + * See Table 4.7.20.2-D. + */ + describe('#generic2()', () => { + describe('attribute: RuntimeVisibleTypeAnnotations', () => { + it('should exist', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[2] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + + assert.notEqual(rvtAttr, undefined) + }) + + it('property: num_annotations should be equal to 6', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[2] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + + assert.equal(rvtAttr.num_annotations, 6) + assert.equal(rvtAttr.annotations.length, 6) + }) + + /** + * Get annotation by type from 'rvtAttr.annotations' + */ + function getAnnotationByType(classFile, rvtAttr, type) { + return rvtAttr.annotations.filter(ann => { + const annType = CPUtil.getString(classFile, ann.type_index) + return annType === type + })[0] + } + + describe('the @A annotation', () => { + it('should exist', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[2] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + const annotation = getAnnotationByType(classFile, rvtAttr, 'LReadMethodsTest_1$A;') + + assert.notEqual(annotation, undefined) + }) + + it('type_path.path_length should be equal to 0', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[2] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + const annotation = getAnnotationByType(classFile, rvtAttr, 'LReadMethodsTest_1$A;') + + assert.equal(annotation.type_path.path_length, 0) + }) + + it('type_path.path should be equal to []', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[2] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + const annotation = getAnnotationByType(classFile, rvtAttr, 'LReadMethodsTest_1$A;') + + assert.deepEqual(annotation.type_path.path, []) + }) + }) + + describe('the @B annotation', () => { + it('should exist', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[2] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + const annotation = getAnnotationByType(classFile, rvtAttr, 'LReadMethodsTest_1$B;') + + assert.notEqual(annotation, undefined) + }) + + it('type_path.path_length should be equal to 1', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[2] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + const annotation = getAnnotationByType(classFile, rvtAttr, 'LReadMethodsTest_1$B;') + + assert.equal(annotation.type_path.path_length, 1) + }) + + it('type_path.path should be equal to [{type_path_kind: 3, type_argument_index: 0}]', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[2] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + const annotation = getAnnotationByType(classFile, rvtAttr, 'LReadMethodsTest_1$B;') + + assert.deepEqual(annotation.type_path.path, [{ type_path_kind: 3, type_argument_index: 0 }]) + }) + }) + + describe('the @C annotation', () => { + it('should exist', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[2] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + const annotation = getAnnotationByType(classFile, rvtAttr, 'LReadMethodsTest_1$C;') + + assert.notEqual(annotation, undefined) + }) + + it('type_path.path_length should be equal to 2', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[2] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + const annotation = getAnnotationByType(classFile, rvtAttr, 'LReadMethodsTest_1$C;') + + assert.equal(annotation.type_path.path_length, 2) + }) + + it('type_path.path should be equal to ', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[2] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + const annotation = getAnnotationByType(classFile, rvtAttr, 'LReadMethodsTest_1$C;') + + assert.deepEqual(annotation.type_path.path, [ + { type_path_kind: 3, type_argument_index: 0 }, + { type_path_kind: 3, type_argument_index: 0 } + ]) + }) + }) + + describe('the @D annotation', () => { + it('should exist', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[2] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + const annotation = getAnnotationByType(classFile, rvtAttr, 'LReadMethodsTest_1$D;') + + assert.notEqual(annotation, undefined) + }) + + it('type_path.path_length should be equal to 3', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[2] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + const annotation = getAnnotationByType(classFile, rvtAttr, 'LReadMethodsTest_1$D;') + + assert.equal(annotation.type_path.path_length, 3) + }) + + it('type_path.path should be equal to [{type_path_kind: 3, type_argument_index: 0}, {type_path_kind: 3, type_argument_index: 0}]', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[2] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + const annotation = getAnnotationByType(classFile, rvtAttr, 'LReadMethodsTest_1$D;') + + assert.deepEqual(annotation.type_path.path, [ + { type_path_kind: 3, type_argument_index: 0 }, + { type_path_kind: 3, type_argument_index: 0 }, + { type_path_kind: 0, type_argument_index: 0 } + ]) + }) + }) + + describe('the @E annotation', () => { + it('should exist', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[2] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + const annotation = getAnnotationByType(classFile, rvtAttr, 'LReadMethodsTest_1$E;') + assert.notEqual(annotation, undefined) + }) + + it('type_path.path_length should be equal to 4', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[2] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + const annotation = getAnnotationByType(classFile, rvtAttr, 'LReadMethodsTest_1$E;') + assert.equal(annotation.type_path.path_length, 4) + }) + + it('type_path.path should be equal to [{type_path_kind: 3, type_argument_index: 0}, {type_path_kind: 3, type_argument_index: 0}, {type_path_kind: 0, type_argument_index: 0}, {type_path_kind: 0, type_argument_index: 0}]', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[2] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + const annotation = getAnnotationByType(classFile, rvtAttr, 'LReadMethodsTest_1$E;') + assert.deepEqual(annotation.type_path.path, [ + { type_path_kind: 3, type_argument_index: 0 }, + { type_path_kind: 3, type_argument_index: 0 }, + { type_path_kind: 0, type_argument_index: 0 }, + { type_path_kind: 0, type_argument_index: 0 } + ]) + }) + }) + + describe('the @F annotation', () => { + it('should exist', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[2] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + const annotation = getAnnotationByType(classFile, rvtAttr, 'LReadMethodsTest_1$F;') + + assert.notEqual(annotation, undefined) + }) + + it('type_path.path_length should be equal to 5', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[2] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + const annotation = getAnnotationByType(classFile, rvtAttr, 'LReadMethodsTest_1$F;') + + assert.equal(annotation.type_path.path_length, 5) + }) + + it('type_path.path should be equal to [{type_path_kind: 3, type_argument_index: 0}, {type_path_kind: 3, type_argument_index: 0}, {type_path_kind: 0, type_argument_index: 0}, {type_path_kind: 0, type_argument_index: 0}, {type_path_kind: 0, type_argument_index: 0}]', async () => { + const classFile = await compileAndRead('ReadMethodsTest_1', code, { + javac_flags: '-parameters' + }) + const method = classFile.methods[2] + const rvtAttr = method.attributes.filter(attr => { + const attrName = CPUtil.getString(classFile, attr.attribute_name_index) + return attrName === 'RuntimeVisibleTypeAnnotations' + })[0] + const annotation = getAnnotationByType(classFile, rvtAttr, 'LReadMethodsTest_1$F;') + + assert.deepEqual(annotation.type_path.path, [ + { type_path_kind: 3, type_argument_index: 0 }, + { type_path_kind: 3, type_argument_index: 0 }, + { type_path_kind: 0, type_argument_index: 0 }, + { type_path_kind: 0, type_argument_index: 0 }, + { type_path_kind: 0, type_argument_index: 0 } + ]) + }) + }) + }) + }) +}) + +describe('test fields', () => { + const code = ` +import java.lang.annotation.*; + +public class ReadFieldTest_0 { + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface Bar { + boolean _boolean(); + String _string(); + int _integer(); + double _double(); + String[] _strArray(); + Nested _nested(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.ANNOTATION_TYPE) + @interface Nested { + String value(); + } + + @Deprecated + @Bar( + _boolean = true, + _string = "quux", + _integer = 4444, + _double = 4433.22, + _strArray = { "foo", "bar" }, + _nested = @Nested("string") + ) + protected static int someValue = 333333; +} + ` + + describe('fields[0]', () => { + it('should be "protected" and "static"', async () => { + let classFile = await compileAndRead('ReadFieldTest_0', code) + let field = classFile.fields[0] + assert((field.access_flags & Modifier.PROTECTED) === Modifier.PROTECTED) + assert((field.access_flags & Modifier.STATIC) === Modifier.STATIC) + }) + + it('should have "Deprecated" attribute.', async () => { + let classFile = await compileAndRead('ReadFieldTest_0', code) + let field = classFile.fields[0] + field.attributes.some(attr => { + return CPUtil.getString(classFile, attr.attribute_name_index) === 'Deprecated' + }) + }) + + it('the name should be equals to "someValue"', async () => { + let classFile = await compileAndRead('ReadFieldTest_0', code) + let field = classFile.fields[0] + assert.equal(CPUtil.getString(classFile, field.name_index), 'someValue') + }) + + it('the descriptor should be equals to "I"', async () => { + let classFile = await compileAndRead('ReadFieldTest_0', code) + let field = classFile.fields[0] + assert.equal(CPUtil.getString(classFile, field.descriptor_index), 'I') + }) + }) +}) + +describe('class Foo', () => { + it('should be public', async () => { + const classFile = await compileAndRead('Foo', `public class Foo {}`) + assert(classFile.access_flags & Modifier.PUBLIC) + }) + + it('constant_pool should not be empty', async () => { + const classFile = await compileAndRead('Foo', `public class Foo {}`) + assert(classFile.constant_pool_count > 0) + }) +}) + +describe('_readConstantEntry()', () => { + describe('ConstantType.INTEGER', () => { + function testInteger(input: number) { + let buf = new ByteBuffer(5) + + buf.writeUint8(ConstantType.INTEGER) + buf.writeUint32(input) + buf.flip() + let cp_info = ClassFile._readConstantPoolEntry(buf) + assert.equal(cp_info.tag, ConstantType.INTEGER) + // @ts-expect-error + cp_info.bytes |= 0 // cast to signed + assert.equal(cp_info.bytes, input) + } + let integerTests = [2147483647, -2147483648, -333, 0, 150] + + integerTests.forEach(function (int) { + it(`With args: (${int})`, () => { + testInteger(int) + }) + }) + }) + + describe('ConstantType.FLOAT', () => { + function testFloat(input: number, expected: number, writeInt?: boolean) { + if (expected === undefined) expected = input + let buf = new ByteBuffer(5) + + buf.writeUint8(ConstantType.FLOAT) + if (writeInt) { + buf.writeInt32(input) + } else { + buf.writeFloat32(input) + } + buf.flip() + let cp_info = ClassFile._readConstantPoolEntry(buf) + let floatValue = CPUtil.u32ToFloat(cp_info.bytes as any) + assert.equal(cp_info.tag, ConstantType.FLOAT) + assert(compareFloatBits(floatValue, expected), floatValue + ' != ' + expected) + } + let floatTestInputs = [ + [55.8734], + [5124124125.939838383], + [0x7f800000, Number.POSITIVE_INFINITY, true], + [0xff800000, Number.NEGATIVE_INFINITY, true], + [0x7f800001, Number.NaN, true], + [0x7f800001 + 100, Number.NaN, true], + [0x7fffffff, Number.NaN, true], + [0xffffffff, Number.NaN, true] + ] + + floatTestInputs.forEach(function (args) { + it(`With args: (${args})`, function () { + // @ts-expect-error + testFloat.apply(null, args) + }) + }) + }) + + describe('ConstantType.LONG', () => { + function testLong(hig, low) { + let buf = new ByteBuffer(5) + buf.writeUint8(ConstantType.LONG) + buf.writeUint32(hig) + buf.writeUint32(low) + buf.flip() + let cp_info = ClassFile._readConstantPoolEntry(buf) + assert.equal(cp_info.tag, ConstantType.LONG) + // @ts-expect-error + assert.equal(cp_info.high_bytes, hig) + // @ts-expect-error + assert.equal(cp_info.low_bytes, low) + } + let longTests = [[0x7fffffff, 0xffffffff]] + + longTests.forEach(function (args) { + it(`With args: (${args})`, () => { + // @ts-expect-error + testLong.apply(null, args) + }) + }) + }) +}) + +function compareFloatBits(f1, f2) { + let buf1 = ByteBuffer.allocate(4), + buf2 = ByteBuffer.allocate(4) + buf1.writeFloat(f1).flip() + buf2.writeFloat(f2).flip() + return buf1.buffer.equals(buf2.buffer) +} + +type CompileAndReadOptions = { + javac_flags?: string + printReadTime?: boolean + printJavap?: boolean +} + +const compileCache = {} + +function compileCacheKey(fileName: string, code: string, options?: Partial): string { + const preimage = `${fileName}:${code}:${JSON.stringify(options || {})}` + return createHash('sha1').update(preimage).digest('hex') +} + +async function compileAndRead( + fileName: string, + code: string, + options?: Partial +): Promise { + if (typeof fileName !== 'string' || !fileName) throw 'Invalid fileName' + if (typeof code !== 'string' || !code) throw 'Invalid code' + + const opts = options || {} + if (!opts.javac_flags) opts.javac_flags = '' + if (!opts.printReadTime) opts.printReadTime = false + if (!opts.printJavap) opts.printJavap = false + + // don't recompile things + const cacheKey = compileCacheKey(fileName, code, opts) + const cached = compileCache[cacheKey] + if (cached) return cached + + // grab a temp root to write the source + const tmpRoot = fs.mkdtempSync(TMP_DIR_PREFIX) + const filename = fileName.endsWith('.java') ? fileName : `${fileName}.java` + const tmpSrc = join(tmpRoot, filename) + fs.writeFileSync(tmpSrc, code, { encoding: 'utf8' }) + + // compile the code + const args = !!opts.javac_flags ? opts.javac_flags.split(' ') : [] + const result = await compileJava([tmpSrc], { + args, + preserveArgPaths: true + }) + expect(result).toBeDefined() + expect(result.result.run.exitCode).toBe(0) + + // grab the class output + const out = result.classes.find(f => f.endsWith(`${fileName}.class`)) + if (!out) { + throw new Error(`Failed to locate expected class in output: ${fileName}.class`) + } + + if (opts.printJavap) { + const javap = JavaToolchain.current().tool('javap') + javap.cwd(result.buildroot) + console.log(' ======= JAVAP ======= ') + console.log((await javap.run(['-v', out])).result.stdout.toString()) + console.log(' ======= JAVAP ======= ') + } + + const classfileResult = (await JavaClassFile.fromFile(out)).raw() + compileCache[cacheKey] = classfileResult + return classfileResult +} + +function getAttribute(source: any, attrName: string, classFile: ClassFile) { + return source.attributes.filter(attr => { + return attrName === CPUtil.getString(classFile, attr.attribute_name_index) + })[0] +} diff --git a/packages/java/tests/java-classfile.test.ts b/packages/java/tests/java-classfile.test.ts new file mode 100644 index 00000000..bcc0c2b3 --- /dev/null +++ b/packages/java/tests/java-classfile.test.ts @@ -0,0 +1,542 @@ +/* + * Copyright (c) 2024 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under the License. + */ + +import { expect, test } from '@jest/globals' +import { JavaClassFile } from '../java-classfile' +import { compileJava } from './testutil.test' +import { existsSync, readFileSync } from 'fs' +import { JavaToolchain } from '../java-home' +import { jvmLevelForTarget, jvmTargetForVersion } from '../java-model' +import { BuiltinModule } from '../javamodules/jdk-modules' + +test('can parse a simple java class', async () => { + const result = await compileJava(['Hello.java']) + expect(result.classes).toHaveLength(1) + const compiledClassfile = result.classes[0] + expect(existsSync(compiledClassfile)).toBe(true) + const parsed = await JavaClassFile.fromFile(compiledClassfile) + expect(parsed).toBeDefined() + expect(parsed.raw()).toBeDefined() +}) + +/// ----- Bytecode Targeting + +test('can determine the bytecode target for a java class', async () => { + // setup expectations + const toolchain = JavaToolchain.current() + const jdkVersion = toolchain.semver().major + const jvmTargetExpected = jvmTargetForVersion(jdkVersion) + const bytecodeTargetExpected = jvmLevelForTarget(jvmTargetExpected) + + // compile + const result = await compileJava(['Hello.java']) + expect(result.classes).toHaveLength(1) + const compiledClassfile = result.classes[0] + expect(existsSync(compiledClassfile)).toBe(true) + const parsed = await JavaClassFile.fromFile(compiledClassfile) + expect(parsed).toBeDefined() + const target = parsed.bytecodeTarget() + expect(target).toBeDefined() + expect(parsed.raw().major_version).toBe(bytecodeTargetExpected) + expect(target).toBe(jvmTargetExpected) +}) + +test('can determine the bytecode target for java class data', async () => { + // setup expectations + const toolchain = JavaToolchain.current() + const jdkVersion = toolchain.semver().major + const jvmTargetExpected = jvmTargetForVersion(jdkVersion) + const bytecodeTargetExpected = jvmLevelForTarget(jvmTargetExpected) + + // compile + const result = await compileJava(['Hello.java']) + expect(result.classes).toHaveLength(1) + const compiledClassfile = result.classes[0] + expect(existsSync(compiledClassfile)).toBe(true) + const compiledClassfileData = readFileSync(compiledClassfile) + const parsed = JavaClassFile.fromData(compiledClassfileData) + expect(parsed).toBeDefined() + const target = parsed.bytecodeTarget() + expect(target).toBeDefined() + expect(parsed.raw().major_version).toBe(bytecodeTargetExpected) + expect(target).toBe(jvmTargetExpected) +}) + +/// ----- Class Naming + +test('can determine the qualified class name for a java class', async () => { + const result = await compileJava(['Hello.java']) + expect(result.classes).toHaveLength(1) + const compiledClassfile = result.classes[0] + expect(existsSync(compiledClassfile)).toBe(true) + const parsed = await JavaClassFile.fromFile(compiledClassfile) + expect(parsed).toBeDefined() + expect(parsed.qualifiedName()).toBe('hello.Hello') +}) + +test('can determine the simple class name for a java class', async () => { + const result = await compileJava(['Hello.java']) + expect(result.classes).toHaveLength(1) + const compiledClassfile = result.classes[0] + expect(existsSync(compiledClassfile)).toBe(true) + const parsed = await JavaClassFile.fromFile(compiledClassfile) + expect(parsed).toBeDefined() + expect(parsed.simpleName()).toBe('Hello') +}) + +test('can determine the package name for a java class', async () => { + const result = await compileJava(['Hello.java']) + expect(result.classes).toHaveLength(1) + const compiledClassfile = result.classes[0] + expect(existsSync(compiledClassfile)).toBe(true) + const parsed = await JavaClassFile.fromFile(compiledClassfile) + expect(parsed).toBeDefined() + expect(parsed.packageName()).toBe('hello') +}) + +/// ----- Modules + +test('can identify a non-module compiled class', async () => { + const result = await compileJava(['Hello.java']) + expect(result.classes).toHaveLength(1) + const compiledClassfile = result.classes[0] + expect(existsSync(compiledClassfile)).toBe(true) + const parsed = await JavaClassFile.fromFile(compiledClassfile) + expect(parsed).toBeDefined() + expect(parsed.isModule()).toBe(false) +}) + +test('can identify a compiled module definition (simple)', async () => { + const result = await compileJava(['Hello.java', 'module-info.java']) + expect(result.classes).toHaveLength(2) + const compiledClassfile = result.classes.find(f => f.endsWith('module-info.class')) as string + expect(existsSync(compiledClassfile)).toBe(true) + const parsed = await JavaClassFile.fromFile(compiledClassfile) + expect(parsed).toBeDefined() + expect(parsed.isModule()).toBe(true) + expect(parsed.moduleInfo()).toBeDefined() + const info = parsed.moduleInfo() + expect(info.name).toBe('sample') + expect(info.version).not.toBeDefined() + expect(info.flags.open).toBe(false) + expect(info.requires).toHaveLength(2) + expect(info.requires[0].module).toBe(BuiltinModule.JAVA_BASE) + expect(info.requires[0].static).toBe(false) + expect(info.requires[0].transitive).toBe(false) + expect(info.requires[1].module).toBe(BuiltinModule.JAVA_LOGGING) + expect(info.requires[1].static).toBe(false) + expect(info.requires[1].transitive).toBe(false) + expect(info.exports).toHaveLength(1) + expect(info.exports[0].package).toBe('hello') + expect(info.exports[0].to).toBeDefined() + expect(info.exports[0].to).toHaveLength(0) + expect(info.opens).toHaveLength(0) + expect(info.provides).toHaveLength(0) + expect(info.uses).toHaveLength(0) +}) + +test('can identify a compiled module definition (complex)', async () => { + const result = await compileJava([ + { from: 'complex/META-INF/services/hello.Service', to: 'META-INF/services/hello.Service' }, + { from: 'complex/module-info.java', to: 'module-info.java' }, + { from: 'complex/Another.java', to: 'Another.java' }, + { from: 'complex/Implementation.java', to: 'Implementation.java' }, + { from: 'complex/Service.java', to: 'Service.java' }, + { from: 'complex/Hello.java', to: 'Hello.java' } + ]) + expect(result.classes).toHaveLength(5) + expect(result.outputs).toHaveLength(6) + expect(result.resources).toHaveLength(1) + const compiledClassfile = result.classes.find(f => f.endsWith('module-info.class')) as string + expect(existsSync(compiledClassfile)).toBe(true) + const parsed = await JavaClassFile.fromFile(compiledClassfile) + expect(parsed).toBeDefined() + expect(parsed.isModule()).toBe(true) + expect(parsed.moduleInfo()).toBeDefined() + const info = parsed.moduleInfo() + expect(info.name).toBe('complex') + expect(info.version).not.toBeDefined() + expect(info.flags.open).toBe(false) + expect(info.requires).toHaveLength(4) + + // `requires java.base` (implied) + expect(info.requires[0].module).toBe(BuiltinModule.JAVA_BASE) + expect(info.requires[0].static).toBe(false) + expect(info.requires[0].transitive).toBe(false) + + // `requires transitive java.compiler` + expect(info.requires[1].module).toBe(BuiltinModule.JAVA_COMPILER) + expect(info.requires[1].static).toBe(false) + expect(info.requires[1].transitive).toBe(true) + + // `requires static java.compiler` + expect(info.requires[2].module).toBe(BuiltinModule.JAVA_DESKTOP) + expect(info.requires[2].static).toBe(true) + expect(info.requires[2].transitive).toBe(false) + + // `requires java.logging` + expect(info.requires[3].module).toBe(BuiltinModule.JAVA_LOGGING) + expect(info.requires[3].static).toBe(false) + expect(info.requires[3].transitive).toBe(false) + + expect(info.exports).toHaveLength(2) + + // `exports hello` + expect(info.exports[0].package).toBe('hello') + expect(info.exports[0].to).toBeDefined() + expect(info.exports[0].to).toHaveLength(0) + + // `exports another to sample` + expect(info.exports[1].package).toBe('another') + expect(info.exports[1].to).toBeDefined() + expect(info.exports[1].to).toHaveLength(1) + expect(info.exports[1].to[0]).toBe('sample') + + expect(info.opens).toHaveLength(2) + + // `opens hello` + expect(info.opens[0].package).toBe('hello') + expect(info.opens[0].to).toBeDefined() + expect(info.opens[0].to).toHaveLength(0) + + // `opens another to sample` + expect(info.opens[1].package).toBe('another') + expect(info.opens[1].to).toBeDefined() + expect(info.opens[1].to).toHaveLength(1) + expect(info.opens[1].to[0]).toBe('sample') + + expect(info.uses).toHaveLength(1) + + // `uses hello.Service` + expect(info.uses[0].service).toBe('hello.Service') + + expect(info.provides).toHaveLength(1) + + // `provides hello.Service with hello.Implementation` + expect(info.provides[0].service).toBeDefined() + expect(info.provides[0].service).toBe('hello.Service') + expect(info.provides[0].with).toBeDefined() + expect(info.provides[0].with).toHaveLength(1) + expect(info.provides[0].with[0]).toBe('hello.Implementation') +}) + +test('can identify top-level module info', async () => { + const result = await compileJava([ + { from: 'complex/META-INF/services/hello.Service', to: 'META-INF/services/hello.Service' }, + { from: 'complex/module-info.java', to: 'module-info.java' }, + { from: 'complex/Another.java', to: 'Another.java' }, + { from: 'complex/Implementation.java', to: 'Implementation.java' }, + { from: 'complex/Service.java', to: 'Service.java' }, + { from: 'complex/Hello.java', to: 'Hello.java' } + ]) + expect(result.classes).toHaveLength(5) + expect(result.outputs).toHaveLength(6) + expect(result.resources).toHaveLength(1) + const compiledClassfile = result.classes.find(f => f.endsWith('module-info.class')) as string + expect(existsSync(compiledClassfile)).toBe(true) + const parsed = await JavaClassFile.fromFile(compiledClassfile) + expect(parsed).toBeDefined() + expect(parsed.isModule()).toBe(true) + expect(parsed.moduleInfo()).toBeDefined() + const info = parsed.moduleInfo() + expect(info.name).toBe('complex') + expect(info.version).not.toBeDefined() + expect(info.flags.open).toBe(false) + expect(info.requires).toHaveLength(4) +}) + +test('can identify top-level module info (open)', async () => { + const result = await compileJava([ + { from: 'open/module-info.java', to: 'module-info.java' }, + { from: 'open/Hello.java', to: 'Hello.java' } + ]) + expect(result.classes).toHaveLength(2) + expect(result.outputs).toHaveLength(2) + expect(result.resources).toHaveLength(0) + const compiledClassfile = result.classes.find(f => f.endsWith('module-info.class')) as string + expect(existsSync(compiledClassfile)).toBe(true) + const parsed = await JavaClassFile.fromFile(compiledClassfile) + expect(parsed).toBeDefined() + expect(parsed.isModule()).toBe(true) + expect(parsed.moduleInfo()).toBeDefined() + const info = parsed.moduleInfo() + expect(info.name).toBe('sample') + expect(info.version).not.toBeDefined() + expect(info.flags.open).toBe(true) + expect(info.requires).toHaveLength(1) +}) + +test('can identify module `requires ...` statements', async () => { + const result = await compileJava([ + { from: 'complex/META-INF/services/hello.Service', to: 'META-INF/services/hello.Service' }, + { from: 'complex/module-info.java', to: 'module-info.java' }, + { from: 'complex/Another.java', to: 'Another.java' }, + { from: 'complex/Implementation.java', to: 'Implementation.java' }, + { from: 'complex/Service.java', to: 'Service.java' }, + { from: 'complex/Hello.java', to: 'Hello.java' } + ]) + expect(result.classes).toHaveLength(5) + expect(result.outputs).toHaveLength(6) + expect(result.resources).toHaveLength(1) + const compiledClassfile = result.classes.find(f => f.endsWith('module-info.class')) as string + expect(existsSync(compiledClassfile)).toBe(true) + const parsed = await JavaClassFile.fromFile(compiledClassfile) + expect(parsed).toBeDefined() + expect(parsed.isModule()).toBe(true) + expect(parsed.moduleInfo()).toBeDefined() + const info = parsed.moduleInfo() + expect(info.name).toBe('complex') + expect(info.version).not.toBeDefined() + expect(info.flags.open).toBe(false) + expect(info.requires).toHaveLength(4) + + // `requires java.base` (implied) + expect(info.requires[0].module).toBe(BuiltinModule.JAVA_BASE) + expect(info.requires[0].static).toBe(false) + expect(info.requires[0].transitive).toBe(false) + + // `requires transitive java.compiler` + expect(info.requires[1].module).toBe(BuiltinModule.JAVA_COMPILER) + expect(info.requires[1].static).toBe(false) + expect(info.requires[1].transitive).toBe(true) + + // `requires static java.compiler` + expect(info.requires[2].module).toBe(BuiltinModule.JAVA_DESKTOP) + expect(info.requires[2].static).toBe(true) + expect(info.requires[2].transitive).toBe(false) + + // `requires java.logging` + expect(info.requires[3].module).toBe(BuiltinModule.JAVA_LOGGING) + expect(info.requires[3].static).toBe(false) + expect(info.requires[3].transitive).toBe(false) +}) + +test('can identify module `exports ...` statements', async () => { + const result = await compileJava([ + { from: 'complex/META-INF/services/hello.Service', to: 'META-INF/services/hello.Service' }, + { from: 'complex/module-info.java', to: 'module-info.java' }, + { from: 'complex/Another.java', to: 'Another.java' }, + { from: 'complex/Implementation.java', to: 'Implementation.java' }, + { from: 'complex/Service.java', to: 'Service.java' }, + { from: 'complex/Hello.java', to: 'Hello.java' } + ]) + expect(result.classes).toHaveLength(5) + expect(result.outputs).toHaveLength(6) + expect(result.resources).toHaveLength(1) + const compiledClassfile = result.classes.find(f => f.endsWith('module-info.class')) as string + expect(existsSync(compiledClassfile)).toBe(true) + const parsed = await JavaClassFile.fromFile(compiledClassfile) + expect(parsed).toBeDefined() + expect(parsed.isModule()).toBe(true) + expect(parsed.moduleInfo()).toBeDefined() + const info = parsed.moduleInfo() + expect(info.name).toBe('complex') + expect(info.version).not.toBeDefined() + expect(info.flags.open).toBe(false) + expect(info.requires).toHaveLength(4) + expect(info.exports).toHaveLength(2) + + // `exports hello` + expect(info.exports[0].package).toBe('hello') + expect(info.exports[0].to).toBeDefined() + expect(info.exports[0].to).toHaveLength(0) +}) + +test('can identify module `exports ... to ...` statements', async () => { + const result = await compileJava([ + { from: 'complex/META-INF/services/hello.Service', to: 'META-INF/services/hello.Service' }, + { from: 'complex/module-info.java', to: 'module-info.java' }, + { from: 'complex/Another.java', to: 'Another.java' }, + { from: 'complex/Implementation.java', to: 'Implementation.java' }, + { from: 'complex/Service.java', to: 'Service.java' }, + { from: 'complex/Hello.java', to: 'Hello.java' } + ]) + expect(result.classes).toHaveLength(5) + expect(result.outputs).toHaveLength(6) + expect(result.resources).toHaveLength(1) + const compiledClassfile = result.classes.find(f => f.endsWith('module-info.class')) as string + expect(existsSync(compiledClassfile)).toBe(true) + const parsed = await JavaClassFile.fromFile(compiledClassfile) + expect(parsed).toBeDefined() + expect(parsed.isModule()).toBe(true) + expect(parsed.moduleInfo()).toBeDefined() + const info = parsed.moduleInfo() + expect(info.name).toBe('complex') + expect(info.version).not.toBeDefined() + expect(info.flags.open).toBe(false) + expect(info.requires).toHaveLength(4) + expect(info.exports).toHaveLength(2) + + // `exports another to sample` + expect(info.exports[1].package).toBe('another') + expect(info.exports[1].to).toBeDefined() + expect(info.exports[1].to).toHaveLength(1) + expect(info.exports[1].to[0]).toBe('sample') +}) + +test('can identify module `opens ...` statements', async () => { + const result = await compileJava([ + { from: 'complex/META-INF/services/hello.Service', to: 'META-INF/services/hello.Service' }, + { from: 'complex/module-info.java', to: 'module-info.java' }, + { from: 'complex/Another.java', to: 'Another.java' }, + { from: 'complex/Implementation.java', to: 'Implementation.java' }, + { from: 'complex/Service.java', to: 'Service.java' }, + { from: 'complex/Hello.java', to: 'Hello.java' } + ]) + expect(result.classes).toHaveLength(5) + expect(result.outputs).toHaveLength(6) + expect(result.resources).toHaveLength(1) + const compiledClassfile = result.classes.find(f => f.endsWith('module-info.class')) as string + expect(existsSync(compiledClassfile)).toBe(true) + const parsed = await JavaClassFile.fromFile(compiledClassfile) + expect(parsed).toBeDefined() + expect(parsed.isModule()).toBe(true) + expect(parsed.moduleInfo()).toBeDefined() + const info = parsed.moduleInfo() + expect(info.name).toBe('complex') + expect(info.version).not.toBeDefined() + expect(info.flags.open).toBe(false) + expect(info.requires).toHaveLength(4) + expect(info.opens).toHaveLength(2) + + // `opens hello` + expect(info.opens[0].package).toBe('hello') + expect(info.opens[0].to).toBeDefined() + expect(info.opens[0].to).toHaveLength(0) +}) + +test('can identify module `opens ... to ...` statements', async () => { + const result = await compileJava([ + { from: 'complex/META-INF/services/hello.Service', to: 'META-INF/services/hello.Service' }, + { from: 'complex/module-info.java', to: 'module-info.java' }, + { from: 'complex/Another.java', to: 'Another.java' }, + { from: 'complex/Implementation.java', to: 'Implementation.java' }, + { from: 'complex/Service.java', to: 'Service.java' }, + { from: 'complex/Hello.java', to: 'Hello.java' } + ]) + expect(result.classes).toHaveLength(5) + expect(result.outputs).toHaveLength(6) + expect(result.resources).toHaveLength(1) + const compiledClassfile = result.classes.find(f => f.endsWith('module-info.class')) as string + expect(existsSync(compiledClassfile)).toBe(true) + const parsed = await JavaClassFile.fromFile(compiledClassfile) + expect(parsed).toBeDefined() + expect(parsed.isModule()).toBe(true) + expect(parsed.moduleInfo()).toBeDefined() + const info = parsed.moduleInfo() + expect(info.name).toBe('complex') + expect(info.version).not.toBeDefined() + expect(info.flags.open).toBe(false) + expect(info.requires).toHaveLength(4) + expect(info.opens).toHaveLength(2) + + // `opens another to sample` + expect(info.opens[1].package).toBe('another') + expect(info.opens[1].to).toBeDefined() + expect(info.opens[1].to).toHaveLength(1) + expect(info.opens[1].to[0]).toBe('sample') +}) + +test('can identify module `uses ...` statements', async () => { + const result = await compileJava([ + { from: 'complex/META-INF/services/hello.Service', to: 'META-INF/services/hello.Service' }, + { from: 'complex/module-info.java', to: 'module-info.java' }, + { from: 'complex/Another.java', to: 'Another.java' }, + { from: 'complex/Implementation.java', to: 'Implementation.java' }, + { from: 'complex/Service.java', to: 'Service.java' }, + { from: 'complex/Hello.java', to: 'Hello.java' } + ]) + expect(result.classes).toHaveLength(5) + expect(result.outputs).toHaveLength(6) + expect(result.resources).toHaveLength(1) + const compiledClassfile = result.classes.find(f => f.endsWith('module-info.class')) as string + expect(existsSync(compiledClassfile)).toBe(true) + const parsed = await JavaClassFile.fromFile(compiledClassfile) + expect(parsed).toBeDefined() + expect(parsed.isModule()).toBe(true) + expect(parsed.moduleInfo()).toBeDefined() + const info = parsed.moduleInfo() + expect(info.name).toBe('complex') + expect(info.version).not.toBeDefined() + expect(info.flags.open).toBe(false) + expect(info.requires).toHaveLength(4) + expect(info.uses).toHaveLength(1) + + // `uses hello.Service` + expect(info.uses[0].service).toBe('hello.Service') +}) + +test('can identify module `provides ... with ...` statements', async () => { + const result = await compileJava([ + { from: 'complex/META-INF/services/hello.Service', to: 'META-INF/services/hello.Service' }, + { from: 'complex/module-info.java', to: 'module-info.java' }, + { from: 'complex/Another.java', to: 'Another.java' }, + { from: 'complex/Implementation.java', to: 'Implementation.java' }, + { from: 'complex/Service.java', to: 'Service.java' }, + { from: 'complex/Hello.java', to: 'Hello.java' } + ]) + expect(result.classes).toHaveLength(5) + expect(result.outputs).toHaveLength(6) + expect(result.resources).toHaveLength(1) + const compiledClassfile = result.classes.find(f => f.endsWith('module-info.class')) as string + expect(existsSync(compiledClassfile)).toBe(true) + const parsed = await JavaClassFile.fromFile(compiledClassfile) + expect(parsed).toBeDefined() + expect(parsed.isModule()).toBe(true) + expect(parsed.moduleInfo()).toBeDefined() + const info = parsed.moduleInfo() + expect(info.name).toBe('complex') + expect(info.version).not.toBeDefined() + expect(info.flags.open).toBe(false) + expect(info.requires).toHaveLength(4) + expect(info.provides).toHaveLength(1) + + // `provides hello.Service with hello.Implementation` + expect(info.provides[0].service).toBeDefined() + expect(info.provides[0].service).toBe('hello.Service') + expect(info.provides[0].with).toBeDefined() + expect(info.provides[0].with).toHaveLength(1) + expect(info.provides[0].with[0]).toBe('hello.Implementation') +}) + +// test("can identify a module's version", async () => { +// const result = await compileAndPackageJar([ +// {from: 'complex/META-INF/services/hello.Service', to: 'META-INF/services/hello.Service'}, +// {from: 'complex/module-info.java', to: 'module-info.java'}, +// {from: 'complex/Another.java', to: 'Another.java'}, +// {from: 'complex/Implementation.java', to: 'Implementation.java'}, +// {from: 'complex/Service.java', to: 'Service.java'}, +// {from: 'complex/Hello.java', to: 'Hello.java'}, +// ], [], { +// moduleVersion: '1.2.3' +// }); +// expect(result.classes).toHaveLength(5); +// expect(result.resources).toHaveLength(1); +// expect(result.outputs).toHaveLength(6); +// expect(result.jar).toBeDefined(); + +// // parse the built module +// const compiledClassfile = result.classes.find((f) => f.endsWith('module-info.class')) as string; +// expect(existsSync(compiledClassfile)).toBe(true); +// const parsed = await JavaClassFile.fromFile(compiledClassfile); +// expect(parsed).toBeDefined(); +// expect(parsed.isModule()).toBe(true); +// expect(parsed.moduleInfo()).toBeDefined(); +// const info = parsed.moduleInfo(); +// expect(info.name).toBe('complex'); +// expect(info.version).not.toBeDefined(); +// expect(info.flags.open).toBe(false); +// expect(info.requires).toHaveLength(4); +// expect(info.provides).toHaveLength(1); +// expect(info.version).toBe('1.2.3'); +// }); diff --git a/packages/java/tests/java-home.test.ts b/packages/java/tests/java-home.test.ts index f8291323..e10410d3 100644 --- a/packages/java/tests/java-home.test.ts +++ b/packages/java/tests/java-home.test.ts @@ -11,20 +11,32 @@ * License for the specific language governing permissions and limitations under the License. */ -import { resolve } from "node:path"; -import { env } from "node:process"; -import { expect, test } from "@jest/globals"; -import { JavaToolchain } from "../javahome"; +import { resolve } from 'node:path' +import { env } from 'node:process' +import { expect, test } from '@jest/globals' +import { JavaToolchain } from '../java-home' -test("obtain the current java toolchain from JAVA_HOME", () => { - expect(JavaToolchain.current()).toBeDefined(); - const toolchain = JavaToolchain.current(); - expect(toolchain.path()).toBeDefined(); - expect(toolchain.versionInfo()).toBeDefined(); - expect(toolchain.version()).toBeDefined(); - expect(toolchain.semver()).toBeDefined(); -}); +test('obtain the current java toolchain from JAVA_HOME', () => { + expect(JavaToolchain.current()).toBeDefined() + const toolchain = JavaToolchain.current() + expect(toolchain.path()).toBeDefined() + expect(toolchain.versionInfo()).toBeDefined() + expect(toolchain.version()).toBeDefined() + expect(toolchain.semver()).toBeDefined() +}) -test("obtain a java toolchain for a path", () => { - expect(JavaToolchain.forPath(resolve(env["JAVA_HOME"] as string))).toBeDefined(); -}); +test('obtain a java toolchain for a path', () => { + expect(JavaToolchain.forPath(resolve(env['JAVA_HOME'] as string))).toBeDefined() +}) + +test('obtain the current java compiler from JAVA_HOME', () => { + expect(JavaToolchain.current().compiler()).toBeDefined() +}) + +test('obtain the current java launcher from JAVA_HOME', () => { + expect(JavaToolchain.current().launcher()).toBeDefined() +}) + +test('obtain the current jar tool from JAVA_HOME', () => { + expect(JavaToolchain.current().tool('jar')).toBeDefined() +}) diff --git a/packages/java/tests/java-jar.test.ts b/packages/java/tests/java-jar.test.ts new file mode 100644 index 00000000..3dee33c7 --- /dev/null +++ b/packages/java/tests/java-jar.test.ts @@ -0,0 +1,314 @@ +/* + * Copyright (c) 2024 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under the License. + */ + +import { describe, expect, test } from '@jest/globals' +import { globSync } from 'glob' +import { existsSync } from 'node:fs' +import JarFile, { JarBuilder, JarCompression, JarEntryType, ZipFileMetadata, JarPredicate, jarMatcher } from '../java-jar' +import { repoJar, repoPath, repoJarByPath } from './testutil.test' +import { manifestPath } from '../java-manifest' + +async function inflateGuavaJar(): Promise { + const { relative, resolved } = repoJar('com.google.guava', 'guava', '33.0.0-jre-jpms') + expect(relative).toBeDefined() + expect(resolved).toBeDefined() + expect(relative).toBe('com/google/guava/guava/33.0.0-jre-jpms/guava-33.0.0-jre-jpms.jar') + expect(resolved.endsWith('com/google/guava/guava/33.0.0-jre-jpms/guava-33.0.0-jre-jpms.jar')).toBeTruthy() + return await JarFile.fromFile(resolved) +} + +const rootRepoPath = repoPath('.') + +const allJars = globSync(`${rootRepoPath}/**/*.jar`) +const guavaJar = inflateGuavaJar() + +const jarEntryTypes = [ + JarEntryType.MANIFEST, + JarEntryType.MODULE, + JarEntryType.CLASS, + JarEntryType.RESOURCE, + JarEntryType.SERVICE, +] + +const samplesByType = { + [`${JarEntryType.MANIFEST}`]: ['META-INF/MANIFEST.MF', 'META-INF/versions/9/MANIFEST.MF'], + [`${JarEntryType.MODULE}`]: ['module-info.class', 'META-INF/versions/9/module-info.class'], + [`${JarEntryType.CLASS}`]: ['com/google/common/hash/Hasher.class', 'module-info.class', 'META-INF/versions/9/com/google/common/hash/Hasher.class'], + [`${JarEntryType.SERVICE}`]: ['META-INF/services/com.google.common.hash.HashFunction', 'META-INF/services/com.google.common.hash.Hasher'], + [`${JarEntryType.RESOURCE}`]: ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt', 'META-INF/some/other/thingy.png', 'some/other/thingy.json'], +} + +function matchPredicateTest(predicate: JarPredicate, subject: string, expected: boolean, props?: Partial) { + expect(predicate({ + name: subject, + compression: JarCompression.IDENTITY, + ...(props || {}), + })).toBe(expected) +} + +function shouldMatch(predicate: JarPredicate, path: string, props?: Partial) { + matchPredicateTest(predicate, path, true, props) +} + +function shouldMatchNone(predicate: JarPredicate, ...cases: JarEntryType[]) { + cases.forEach((type) => { + samplesByType[type].forEach((path) => { + shouldNotMatch(predicate, path) + }) + }) +} + +function jarPathToFilename(path: string): string { + const relative = path.slice(rootRepoPath.length + 1) + const parts = relative.split('/') + return parts.at(-1) as string +} + +function shouldNotMatch(predicate: JarPredicate, path: string, props?: Partial) { + matchPredicateTest(predicate, path, false, props) +} + +function shouldMatchAll(predicate: JarPredicate, ...cases: JarEntryType[]) { + cases.forEach((type) => { + samplesByType[type].forEach((path) => { + shouldMatch(predicate, path) + }) + }) +} + +describe('jar', () => { + test('symbols should be defined', () => { + [ + JarFile, + jarMatcher, + ].forEach((value) => { + expect(value).toBeDefined() + }) + }) + + test('should be able to construct an empty jar builder', () => { + expect(JarFile.builder()).toBeDefined() + expect(JarFile.builder()).toBeInstanceOf(JarBuilder) + }) + + test('should be able to inflate a valid jar', async () => { + expect(await guavaJar).toBeDefined() + }) + + test('should be able to interrogate an inflated jar file', async () => { + const inflated = await guavaJar + expect(inflated.mainClass).toBe(null) + expect(inflated.automaticModuleName).toBe(null) + expect(inflated.multiRelease).toBe(true) + expect(inflated.classAtName('com.google.common.hash.Hasher')).toBeDefined() + }) + + describe('repository jars', () => { + allJars.forEach((absolute) => { + const relativePath = absolute.slice(rootRepoPath.length + 1) + describe(jarPathToFilename(absolute), () => { + + test('jar exists', () => { + expect(relativePath).toBeDefined() + expect(absolute.endsWith(relativePath)).toBeTruthy() + expect(existsSync(absolute)).toBe(true) + }) + test('jar can be read', async () => { + const { relative, resolved } = repoJarByPath(relativePath) + expect(relative).toBeDefined() + expect(resolved).toBeDefined() + const jarFile = await JarFile.fromFile(resolved) + expect(jarFile).toBeDefined() + }) + }) + }) + }) + + describe('predicate matchers', () => { + test('should be able to match nothing with `none()`', () => { + jarEntryTypes.forEach((type) => { + shouldMatchNone(jarMatcher.none(), type) + }) + }) + + test('should be able to match everything with `all()`', () => { + jarEntryTypes.forEach((type) => { + shouldMatchAll(jarMatcher.all(), type) + }) + }) + + // -- predicate: classes + + test('should be able to match classes with `classes()`', () => { + shouldMatchAll(jarMatcher.classes(), JarEntryType.CLASS) + }) + + test('classes should match modules', () => { + shouldMatchAll(jarMatcher.classes(), JarEntryType.MODULE) + }) + + test('classes should not match manifests', () => { + shouldMatchNone(jarMatcher.classes(), JarEntryType.MANIFEST) + }) + + test('classes should not match services', () => { + shouldMatchNone(jarMatcher.classes(), JarEntryType.SERVICE) + }) + + test('classes should not match resources', () => { + shouldMatchNone(jarMatcher.classes(), JarEntryType.RESOURCE) + }) + + // -- predicate: services + + test('should be able to match services with `services()`', () => { + shouldMatchAll(jarMatcher.services(), JarEntryType.SERVICE) + }) + + test('services should not match modules', () => { + shouldMatchNone(jarMatcher.services(), JarEntryType.MODULE) + }) + + test('services should not match manifests', () => { + shouldMatchNone(jarMatcher.services(), JarEntryType.MANIFEST) + }) + + test('services should not match classes', () => { + shouldMatchNone(jarMatcher.services(), JarEntryType.CLASS) + }) + + test('services should not match resources', () => { + shouldMatchNone(jarMatcher.services(), JarEntryType.RESOURCE) + }) + + // -- predicate: resources + + test('should be able to match resources with `resources()`', () => { + shouldMatchAll(jarMatcher.resources(), JarEntryType.RESOURCE) + }) + + test('resources should match manifests', () => { + shouldMatchAll(jarMatcher.resources(), JarEntryType.MANIFEST) + }) + + test('resources should match services', () => { + shouldMatchAll(jarMatcher.resources(), JarEntryType.SERVICE) + }) + + test('resources should not match modules', () => { + shouldMatchNone(jarMatcher.resources(), JarEntryType.MODULE) + }) + + test('resources should not match classes', () => { + shouldMatchNone(jarMatcher.resources(), JarEntryType.CLASS) + }) + + // -- predicate: root manifest + + test('should be able to match manifest with `manifest()`', () => { + shouldMatch(jarMatcher.manifest(), manifestPath) + }) + + test('manifest should not match resources', () => { + shouldMatchNone(jarMatcher.manifest(), JarEntryType.RESOURCE) + }) + + test('manifest should not match services', () => { + shouldMatchNone(jarMatcher.manifest(), JarEntryType.SERVICE) + }) + + test('manifest should not match modules', () => { + shouldMatchNone(jarMatcher.manifest(), JarEntryType.MODULE) + }) + + test('manifest should not match classes', () => { + shouldMatchNone(jarMatcher.manifest(), JarEntryType.CLASS) + }) + + test('manifest should not match non-main manifests', () => { + shouldNotMatch(jarMatcher.manifest(), 'META-INF/versions/9/MANIFEST.MF') + }) + + // -- predicate: all manifests + + test('should be able to match all manifests with `allManifests()`', () => { + shouldMatchAll(jarMatcher.allManifests(), JarEntryType.MANIFEST) + }) + + test('manifests should not match resources', () => { + shouldMatchNone(jarMatcher.allManifests(), JarEntryType.RESOURCE) + }) + + test('manifests should not match services', () => { + shouldMatchNone(jarMatcher.allManifests(), JarEntryType.SERVICE) + }) + + test('manifests should not match modules', () => { + shouldMatchNone(jarMatcher.allManifests(), JarEntryType.MODULE) + }) + + test('manifests should not match classes', () => { + shouldMatchNone(jarMatcher.allManifests(), JarEntryType.CLASS) + }) + + // -- predicate: module-info + + test('should be able to match module-info with `moduleInfo()`', () => { + shouldMatch(jarMatcher.moduleInfo(), 'module-info.class') + }) + + test('module should not match versioned modules', () => { + shouldNotMatch(jarMatcher.moduleInfo(), 'META-INF/versions/9/module-info.class') + }) + + test('module should not match manifests', () => { + shouldMatchNone(jarMatcher.moduleInfo(), JarEntryType.MANIFEST) + }) + + test('module should not match services', () => { + shouldMatchNone(jarMatcher.moduleInfo(), JarEntryType.SERVICE) + }) + + test('module should not match resources', () => { + shouldMatchNone(jarMatcher.moduleInfo(), JarEntryType.RESOURCE) + }) + + test('module should not match classes', () => { + shouldNotMatch(jarMatcher.moduleInfo(), 'com/google/common/hash/Hasher.class') + shouldNotMatch(jarMatcher.moduleInfo(), 'META-INF/versions/9/com/google/common/hash/Hasher.class') + }) + + // -- predicate: all module-infos + + test('should be able to match all module-infos with `allModuleInfos()`', () => { + shouldMatchAll(jarMatcher.allModuleInfos(), JarEntryType.MODULE) + }) + + test('modules should not match manifests', () => { + shouldMatchNone(jarMatcher.allModuleInfos(), JarEntryType.MANIFEST) + }) + + test('modules should not match services', () => { + shouldMatchNone(jarMatcher.allModuleInfos(), JarEntryType.SERVICE) + }) + + test('modules should not match resources', () => { + shouldMatchNone(jarMatcher.allModuleInfos(), JarEntryType.RESOURCE) + }) + + test('modules should not match classes', () => { + shouldNotMatch(jarMatcher.allModuleInfos(), 'com/google/common/hash/Hasher.class') + shouldNotMatch(jarMatcher.allModuleInfos(), 'META-INF/versions/9/com/google/common/hash/Hasher.class') + }) + }) +}) diff --git a/packages/java/tests/java-manifest.test.ts b/packages/java/tests/java-manifest.test.ts new file mode 100644 index 00000000..ceffaeb0 --- /dev/null +++ b/packages/java/tests/java-manifest.test.ts @@ -0,0 +1,298 @@ +/* + * Copyright (c) 2024 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under the License. + */ + +import { describe, expect, test } from '@jest/globals' +import JarManifest, { JarManifestBuilder, JarManifestKey } from '../java-manifest' + +describe('jar manifests', () => { + test('should be constructable in empty form', () => { + expect(JarManifest.builder()).toBeDefined() + expect(JarManifest.builder()).toBeInstanceOf(JarManifestBuilder) + }) + + test('should be constructable from scratch', () => { + const builder = JarManifest.builder() + builder.add(JarManifestKey.MAIN_CLASS, 'com.example.Main') + builder.add(JarManifestKey.IMPLEMENTATION_TITLE, 'Example') + const manifest = builder.build() + expect(manifest).toBeDefined() + expect(manifest).toBeInstanceOf(JarManifest) + expect(manifest.get(JarManifestKey.MAIN_CLASS)).toEqual('com.example.Main') + }) + + test('should be serializable into a string jar manifest', () => { + const builder = JarManifest.builder() + builder.add(JarManifestKey.MAIN_CLASS, 'com.example.Main') + builder.add(JarManifestKey.IMPLEMENTATION_TITLE, 'Example') + const manifest = builder.build() + expect(manifest).toBeDefined() + expect(manifest).toBeInstanceOf(JarManifest) + expect(manifest.get(JarManifestKey.MAIN_CLASS)).toEqual('com.example.Main') + const serialized = manifest.serializeManifest() + expect(serialized).toBeDefined() + expect(typeof serialized).toBe('string') + expect(serialized).toContain('Main-Class: com.example.Main') + expect(serialized).toContain('Implementation-Title: Example') + // prettier-ignore + expect(serialized).toEqual(`Manifest-Version: 1.1 +Created-By: javatools/dev +Main-Class: com.example.Main +Implementation-Title: Example +`) + }) + + test('should be parseable from raw data', async () => { + const builder = JarManifest.builder() + builder.add(JarManifestKey.MAIN_CLASS, 'com.example.Main') + builder.add(JarManifestKey.IMPLEMENTATION_TITLE, 'Example') + const manifest = builder.build() + expect(manifest).toBeDefined() + expect(manifest).toBeInstanceOf(JarManifest) + expect(manifest.get(JarManifestKey.MAIN_CLASS)).toEqual('com.example.Main') + const serialized = manifest.serializeManifest() + expect(serialized).toBeDefined() + expect(typeof serialized).toBe('string') + expect(serialized).toContain('Main-Class: com.example.Main') + expect(serialized).toContain('Implementation-Title: Example') + // prettier-ignore + expect(serialized).toEqual(`Manifest-Version: 1.1 +Created-By: javatools/dev +Main-Class: com.example.Main +Implementation-Title: Example +`) + // try parsing from the serialized data + const parsed = await JarManifest.fromData(Buffer.from(serialized)) + expect(parsed).toBeDefined() + expect(parsed).toBeInstanceOf(JarManifest) + expect(parsed.get(JarManifestKey.MAIN_CLASS)).toEqual('com.example.Main') + expect(parsed.get(JarManifestKey.IMPLEMENTATION_TITLE)).toEqual('Example') + }) + + test('should be interrogable for well-known top-level values', async () => { + const builder = JarManifest.builder() + builder.add(JarManifestKey.MAIN_CLASS, 'com.example.Main') + builder.add(JarManifestKey.IMPLEMENTATION_TITLE, 'Example') + const manifest = builder.build() + expect(manifest).toBeDefined() + expect(manifest).toBeInstanceOf(JarManifest) + expect(manifest.get(JarManifestKey.MAIN_CLASS)).toEqual('com.example.Main') + const serialized = manifest.serializeManifest() + expect(serialized).toBeDefined() + expect(typeof serialized).toBe('string') + expect(serialized).toContain('Main-Class: com.example.Main') + expect(serialized).toContain('Implementation-Title: Example') + // prettier-ignore + expect(serialized).toEqual(`Manifest-Version: 1.1 +Created-By: javatools/dev +Main-Class: com.example.Main +Implementation-Title: Example +`) + // try parsing from the serialized data + const parsed = await JarManifest.fromData(Buffer.from(serialized)) + expect(parsed).toBeDefined() + expect(parsed).toBeInstanceOf(JarManifest) + expect(parsed.mainClass).toEqual('com.example.Main') + expect(parsed.createdBy).toBe('javatools/dev') + expect(parsed.get(JarManifestKey.IMPLEMENTATION_TITLE)).toEqual('Example') + }) + + test('should be buildable with sectional values', () => { + const builder = JarManifest.builder() + builder.add(JarManifestKey.MAIN_CLASS, 'com.example.Main') + builder.add(JarManifestKey.IMPLEMENTATION_TITLE, 'Example') + builder.addQualified('something', JarManifestKey.SEALED, 'true') + const manifest = builder.build() + expect(manifest).toBeDefined() + expect(manifest).toBeInstanceOf(JarManifest) + expect(manifest.get(JarManifestKey.MAIN_CLASS)).toEqual('com.example.Main') + expect(manifest.getQualified('something', JarManifestKey.SEALED)).toEqual('true') + }) + + test('should properly serialize sectional values', () => { + const builder = JarManifest.builder() + builder.add(JarManifestKey.MAIN_CLASS, 'com.example.Main') + builder.add(JarManifestKey.IMPLEMENTATION_TITLE, 'Example') + builder.addQualified('something', JarManifestKey.SEALED, 'true') + const manifest = builder.build() + expect(manifest).toBeDefined() + expect(manifest).toBeInstanceOf(JarManifest) + expect(manifest.get(JarManifestKey.MAIN_CLASS)).toEqual('com.example.Main') + expect(manifest.getQualified('something', JarManifestKey.SEALED)).toEqual('true') + + const serialized = manifest.serializeManifest() + expect(serialized).toBeDefined() + expect(typeof serialized).toBe('string') + expect(serialized).toContain('Main-Class: com.example.Main') + expect(serialized).toContain('Implementation-Title: Example') + expect(serialized).toContain('Name: something') + expect(serialized).toContain('Sealed: true') + // prettier-ignore + expect(serialized).toEqual(`Manifest-Version: 1.1 +Created-By: javatools/dev +Main-Class: com.example.Main +Implementation-Title: Example +Name: something +Sealed: true +`) + }) + + test('should be interrogable for sectional values', () => { + const builder = JarManifest.builder() + builder.add(JarManifestKey.MAIN_CLASS, 'com.example.Main') + builder.add(JarManifestKey.IMPLEMENTATION_TITLE, 'Example') + builder.addQualified('something', JarManifestKey.SEALED, 'true') + const manifest = builder.build() + expect(manifest).toBeDefined() + expect(manifest).toBeInstanceOf(JarManifest) + expect(manifest.get(JarManifestKey.MAIN_CLASS)).toEqual('com.example.Main') + expect(manifest.getQualified('something', JarManifestKey.SEALED)).toEqual('true') + + const serialized = manifest.serializeManifest() + expect(serialized).toBeDefined() + expect(typeof serialized).toBe('string') + expect(serialized).toContain('Main-Class: com.example.Main') + expect(serialized).toContain('Implementation-Title: Example') + expect(serialized).toContain('Name: something') + expect(serialized).toContain('Sealed: true') + // prettier-ignore + expect(serialized).toEqual(`Manifest-Version: 1.1 +Created-By: javatools/dev +Main-Class: com.example.Main +Implementation-Title: Example +Name: something +Sealed: true +`) + // interrogate the manifest for the values + expect(manifest.mainClass).toEqual('com.example.Main') + expect(manifest.get(JarManifestKey.IMPLEMENTATION_TITLE)).toEqual('Example') + expect(manifest.getQualified('something', JarManifestKey.SEALED)).toEqual('true') + expect(manifest.getQualified('another', JarManifestKey.SEALED)).toBe(null) + expect(manifest.multiRelease).toBe(false) + }) + + test('should properly detect multi-release jars', () => { + const builder = JarManifest.builder() + builder.add(JarManifestKey.MAIN_CLASS, 'com.example.Main') + builder.add(JarManifestKey.IMPLEMENTATION_TITLE, 'Example') + builder.add(JarManifestKey.MULTI_RELEASE, true) + builder.addQualified('something', JarManifestKey.SEALED, 'true') + const manifest = builder.build() + expect(manifest).toBeDefined() + expect(manifest).toBeInstanceOf(JarManifest) + expect(manifest.get(JarManifestKey.MAIN_CLASS)).toEqual('com.example.Main') + expect(manifest.getQualified('something', JarManifestKey.SEALED)).toEqual('true') + + const serialized = manifest.serializeManifest() + expect(serialized).toBeDefined() + expect(typeof serialized).toBe('string') + expect(serialized).toContain('Main-Class: com.example.Main') + expect(serialized).toContain('Multi-Release: true') + expect(serialized).toContain('Implementation-Title: Example') + expect(serialized).toContain('Name: something') + expect(serialized).toContain('Sealed: true') + // prettier-ignore + expect(serialized).toEqual(`Manifest-Version: 1.1 +Created-By: javatools/dev +Main-Class: com.example.Main +Implementation-Title: Example +Multi-Release: true +Name: something +Sealed: true +`) + // interrogate the manifest for the values + expect(manifest.mainClass).toEqual('com.example.Main') + expect(manifest.get(JarManifestKey.IMPLEMENTATION_TITLE)).toEqual('Example') + expect(manifest.getQualified('something', JarManifestKey.SEALED)).toEqual('true') + expect(manifest.getQualified('another', JarManifestKey.SEALED)).toBe(null) + expect(manifest.multiRelease).toBe(true) + }) + + test('should properly detect automatic module name', () => { + const builder = JarManifest.builder() + builder.add(JarManifestKey.MAIN_CLASS, 'com.example.Main') + builder.add(JarManifestKey.IMPLEMENTATION_TITLE, 'Example') + builder.add(JarManifestKey.MULTI_RELEASE, true) + builder.add(JarManifestKey.AUTOMATIC_MODULE_NAME, 'example') + builder.addQualified('something', JarManifestKey.SEALED, 'true') + const manifest = builder.build() + expect(manifest).toBeDefined() + expect(manifest).toBeInstanceOf(JarManifest) + expect(manifest.get(JarManifestKey.MAIN_CLASS)).toEqual('com.example.Main') + expect(manifest.getQualified('something', JarManifestKey.SEALED)).toEqual('true') + + const serialized = manifest.serializeManifest() + expect(serialized).toBeDefined() + expect(typeof serialized).toBe('string') + expect(serialized).toContain('Main-Class: com.example.Main') + expect(serialized).toContain('Multi-Release: true') + expect(serialized).toContain('Automatic-Module-Name: example') + expect(serialized).toContain('Implementation-Title: Example') + expect(serialized).toContain('Name: something') + expect(serialized).toContain('Sealed: true') + // prettier-ignore + expect(serialized).toEqual(`Manifest-Version: 1.1 +Created-By: javatools/dev +Main-Class: com.example.Main +Implementation-Title: Example +Multi-Release: true +Automatic-Module-Name: example +Name: something +Sealed: true +`) + // interrogate the manifest for the values + expect(manifest.mainClass).toEqual('com.example.Main') + expect(manifest.get(JarManifestKey.IMPLEMENTATION_TITLE)).toEqual('Example') + expect(manifest.getQualified('something', JarManifestKey.SEALED)).toEqual('true') + expect(manifest.getQualified('another', JarManifestKey.SEALED)).toBe(null) + expect(manifest.multiRelease).toBe(true) + expect(manifest.automaticModuleName).toBe('example') + }) + + test('should set manifest version to latest by default', () => { + const builder = JarManifest.builder() + builder.add(JarManifestKey.MAIN_CLASS, 'com.example.Main') + builder.add(JarManifestKey.IMPLEMENTATION_TITLE, 'Example') + const manifest = builder.build() + expect(manifest).toBeDefined() + expect(manifest).toBeInstanceOf(JarManifest) + expect(manifest.get(JarManifestKey.MAIN_CLASS)).toEqual('com.example.Main') + expect(manifest.get(JarManifestKey.MANIFEST_VERSION)).toEqual('1.1') + expect(manifest.manifestVersion).toBe('1.1') + }) + + test('should be able to check for value membership', () => { + const builder = JarManifest.builder() + builder.add(JarManifestKey.MAIN_CLASS, 'com.example.Main') + builder.add(JarManifestKey.IMPLEMENTATION_TITLE, 'Example') + const manifest = builder.build() + expect(manifest).toBeDefined() + expect(manifest).toBeInstanceOf(JarManifest) + expect(manifest.has(JarManifestKey.MAIN_CLASS)).toBe(true) + expect(manifest.has(JarManifestKey.IMPLEMENTATION_TITLE)).toBe(true) + expect(manifest.has(JarManifestKey.MANIFEST_VERSION)).toBe(true) + expect(manifest.has(JarManifestKey.MULTI_RELEASE)).toBe(false) + }) + + test('should be able to obtain raw manifest data', () => { + const builder = JarManifest.builder() + builder.add(JarManifestKey.MAIN_CLASS, 'com.example.Main') + builder.add(JarManifestKey.IMPLEMENTATION_TITLE, 'Example') + const manifest = builder.build() + expect(manifest).toBeDefined() + expect(manifest).toBeInstanceOf(JarManifest) + expect(manifest.get(JarManifestKey.MAIN_CLASS)).toEqual('com.example.Main') + expect(manifest.get(JarManifestKey.MANIFEST_VERSION)).toEqual('1.1') + expect(manifest.manifestVersion).toBe('1.1') + expect(manifest.rawManifest).toBeDefined() + }) +}) diff --git a/packages/java/tests/java-toolchain.test.ts b/packages/java/tests/java-toolchain.test.ts new file mode 100644 index 00000000..b2a60262 --- /dev/null +++ b/packages/java/tests/java-toolchain.test.ts @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2024 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under the License. + */ + +import { resolve } from 'node:path' +import { env } from 'node:process' +import { writeFileSync, readFileSync, existsSync } from 'node:fs' +import { describe, expect, test, beforeEach, jest } from '@jest/globals' +import { mockPath, testTmpdir, tmpdirPath, compileJava, compileAndPackageJar } from './testutil.test' +import { JvmTarget, JvmPlatform, JavaToolchainVendor } from '../java-model' +import { JavaToolchain } from '../java-home' +import { ToolError } from '../toolchain/abstract' +import { repositoryForToolchain } from '../toolchain/repositories' + +let errorMock: jest.SpiedFunction + +describe('toolchain', () => { + beforeEach(() => { + errorMock = jest.spyOn(console, 'error').mockImplementation(() => {}) + }) + + test('obtain a java compiler from the current toolchain', () => { + expect(JavaToolchain.current()).toBeDefined() + const toolchain = JavaToolchain.current() + expect(toolchain.path()).toBeDefined() + expect(toolchain.versionInfo()).toBeDefined() + expect(toolchain.version()).toBeDefined() + expect(toolchain.semver()).toBeDefined() + expect(errorMock).not.toHaveBeenCalled() + }) + + test('obtain a java compiler from the current toolchain', () => { + const toolchain = JavaToolchain.current() + expect(toolchain.compiler()).toBeDefined() + expect(errorMock).not.toHaveBeenCalled() + }) + + test('obtain a java launcher from the current toolchain', () => { + const toolchain = JavaToolchain.current() + expect(toolchain.launcher()).toBeDefined() + expect(errorMock).not.toHaveBeenCalled() + }) + + test('obtain a java toolchain for a path', () => { + expect(JavaToolchain.forPath(resolve(env['JAVA_HOME'] as string))).toBeDefined() + expect(errorMock).not.toHaveBeenCalled() + }) + + test('can run `javac -version` without failing', async () => { + const javac = JavaToolchain.current().compiler() + const result = await javac.compile(['-version']) + expect(result).toBeDefined() + expect(result.run).toBeDefined() + expect(result.run.exitCode).toBe(0) + expect(errorMock).not.toHaveBeenCalled() + }) + + test('can run `java -version` without failing', async () => { + const java = JavaToolchain.current().launcher() + const result = await java.launch(['-version']) + expect(result).toBeDefined() + expect(result.run).toBeDefined() + expect(result.run.exitCode).toBe(0) + expect(errorMock).not.toHaveBeenCalled() + }) + + test('can run `jar -version` without failing', async () => { + const jar = JavaToolchain.current().tool('jar') + const result = await jar.run(['--version']) + expect(result).toBeDefined() + expect(result.exitCode).toBe(0) + expect(errorMock).not.toHaveBeenCalled() + }) + + test('can run `javac` to compile some java', async () => { + const tmpdir = await testTmpdir() + const srcpath = tmpdirPath(tmpdir, 'Hello.java') + const src = readFileSync(mockPath('compiler/Hello.java'), 'utf-8') + writeFileSync(srcpath, src) + + // source file should exist + expect(existsSync(srcpath)).toBe(true) + + // compiled class file should not exist + const classpath = tmpdirPath(tmpdir, 'Hello.class') + expect(existsSync(classpath)).toBe(false) + + // ok, run the compiler with the tmpdir as cwd + const compiler = JavaToolchain.current().compiler() + compiler.cwd(tmpdir) + const result = await compiler.compile(['Hello.java']) + + // result should be successful + expect(result).toBeDefined() + expect(result.run).toBeDefined() + expect(result.run.exitCode).toBe(0) + + // source file should still exist + expect(existsSync(srcpath)).toBe(true) + + // compiled class should exist now + expect(existsSync(classpath)).toBe(true) + expect(errorMock).not.toHaveBeenCalled() + }) + + test('can run `javac` testutil to compile regular java', async () => { + const result = await compileJava(['Hello.java']) + expect(result.classes).toHaveLength(1) + expect(errorMock).not.toHaveBeenCalled() + }) + + test('can run `javac` to compile modular java', async () => { + const result = await compileJava(['module-info.java', 'Hello.java']) + expect(result.classes).toHaveLength(2) + expect(errorMock).not.toHaveBeenCalled() + }) + + test('can run `javac` to compile modular java with extended module attributes', async () => { + const result = await compileJava(['module-info.java', 'Hello.java'], { + args: ['--module-source-path=$(tmpdir)/src', '--module=sample'] + }) + expect(result.classes).toHaveLength(2) + expect(errorMock).not.toHaveBeenCalled() + }) + + test('captures tool run failures', async () => { + const tool = JavaToolchain.current().compiler() + tool.logs(false) + let caught = false + let err: Error | ToolError | any | undefined + + try { + await tool.compile(['--some-non-existent-flag']) + } catch (error) { + caught = true + err = error + } + + expect(caught).toBe(true) + expect(err).toBeDefined() + expect(err instanceof ToolError).toBe(true) + expect(err.run).toBeDefined() + expect(err.run.exitCode).not.toBe(0) + expect(err.explain()).toBeDefined() + expect(err.explain()).toContain('javac') + expect(err.explain()).toContain(`exit code ${err.run.exitCode}`) + expect(errorMock).not.toHaveBeenCalled() + }) + + test('captures and logs tool run failures to `console.error` when instructed', async () => { + const tool = JavaToolchain.current().compiler() + tool.logs(true) + let caught = false + let err: Error | ToolError | any | undefined + + try { + await tool.compile(['--some-non-existent-flag']) + } catch (error) { + caught = true + err = error + } + + expect(caught).toBe(true) + expect(err).toBeDefined() + expect(err instanceof ToolError).toBe(true) + expect(err.run).toBeDefined() + expect(err.run.exitCode).not.toBe(0) + expect(err.explain()).toBeDefined() + expect(err.explain()).toContain('javac') + expect(err.explain()).toContain(`exit code ${err.run.exitCode}`) + expect(errorMock).toHaveBeenCalledTimes(1) + }) + + test('can run `javac` and `jar` to compile modular java (complex)', async () => { + const result = await compileAndPackageJar([ + { from: 'complex/META-INF/services/hello.Service', to: 'META-INF/services/hello.Service' }, + { from: 'complex/module-info.java', to: 'module-info.java' }, + { from: 'complex/Another.java', to: 'Another.java' }, + { from: 'complex/Implementation.java', to: 'Implementation.java' }, + { from: 'complex/Service.java', to: 'Service.java' }, + { from: 'complex/Hello.java', to: 'Hello.java' } + ]) + expect(result.classes).toHaveLength(5) + expect(result.resources).toHaveLength(1) + expect(result.outputs).toHaveLength(6) + }) +}) + +describe('repositories', () => { + test('can resolve a graalvm download URL', () => { + const out = repositoryForToolchain(JavaToolchainVendor.GRAALVM, JvmTarget.JDK_22, JvmPlatform.LINUX_AMD64) + expect(out.toString()).toBe('https://download.oracle.com/graalvm/22/latest/graalvm-jdk-22_linux-x64_bin.tar.gz') + }) + + test('throws if the vendor is not recognized', () => { + expect(() => { + repositoryForToolchain('not-a-vendor' as any, JvmTarget.JDK_22, JvmPlatform.LINUX_AMD64) + }).toThrow() + }) + + test('throws if the version is not recognized', () => { + expect(() => { + repositoryForToolchain(JavaToolchainVendor.GRAALVM, '99' as JvmTarget, JvmPlatform.LINUX_AMD64) + }).toThrow() + }) + + test('throws if the platform is not recognized', () => { + expect(() => { + repositoryForToolchain(JavaToolchainVendor.GRAALVM, JvmTarget.JDK_22, 'not-supported' as JvmPlatform) + }).toThrow() + }) +}) diff --git a/packages/java/tests/sanity.test.ts b/packages/java/tests/sanity.test.ts deleted file mode 100644 index df2141cd..00000000 --- a/packages/java/tests/sanity.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright (c) 2024 Elide Technologies, Inc. - * - * Licensed under the MIT license (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://opensource.org/license/mit/ - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under the License. - */ - -import { expect, test } from "@jest/globals"; - -test("2 + 2", () => { - expect(2 + 2).toBe(4); -}); diff --git a/packages/java/tests/testutil.test.ts b/packages/java/tests/testutil.test.ts new file mode 100644 index 00000000..8fe0e0a0 --- /dev/null +++ b/packages/java/tests/testutil.test.ts @@ -0,0 +1,364 @@ +/* + * Copyright (c) 2024 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under the License. + */ + +import { dirname, isAbsolute, join, normalize, resolve } from 'node:path' +import { writeFileSync, readFileSync, existsSync } from 'node:fs' +import { test, expect } from '@jest/globals' + +import { JavaToolchain } from '../java-home' +import { mkdir, mkdtemp } from 'node:fs/promises' +import { CompileResult } from '../toolchain/compiler' +import { glob } from 'glob' + +const compileDebugLogs = false + +const manifestDefaultLines = ['Manifest-Version: 1.0', 'Created-By: 1.0.0 (Elide Technologies, Inc.)'] + +export function mockPath(path: string) { + return resolve(normalize(join(__dirname, path))) +} + +export function repoPath(path: string) { + return resolve(normalize(join(__dirname, '..', '..', '..', 'repository', path))) +} + +export function repoJar(group: string, artifact: string, version: string): { relative: string, resolved: string } { + const pathSegments: string[] = [] + const groupSegments = group.split('.') + pathSegments.push(...groupSegments) + pathSegments.push(artifact) + pathSegments.push(version) + pathSegments.push(`${artifact}-${version}.jar`) + return repoJarByPath(join(...pathSegments)) +} + +export function repoJarByPath(path: string): { relative: string, resolved: string } { + return { + resolved: repoPath(path), + relative: path, + } +} +export async function testTmpdir(): Promise { + return await mkdtemp('/tmp/java-toolchain-test-') +} + +export function tmpdirPath(dir: string, path: string) { + return normalize(join(dir, path)) +} + +export function currentToolchain(): JavaToolchain { + return JavaToolchain.current() +} + +export function classpathForSourcePath(path: string): string { + return path.replace(/\.java$/, '.class') +} + +function compileDebug(...args: any[]) { + if (compileDebugLogs) { + console.log(...args) + } +} + +export type CompileJavaOptions = { + args?: string[] + trimPath?: string + preserveArgPaths?: boolean + modular?: boolean +} + +export type CompilePackageJarOptions = CompileJavaOptions & { + jarName?: string + moduleVersion?: string + modulePath?: string + hashModules?: string + jarArgs?: string[] + mainClass?: string +} + +function maybeTrimSourcePath(prefix: string, path: string): string { + if (prefix) { + const rewrite = path.replace(prefix, '') + compileDebug(`replacing prefix: ${path} → ${rewrite}`) + return rewrite + } + return path +} + +/** + * Find the longest common substring within the array of strings + */ +function longestCommonSubstring(entries: string[]): string | null { + if (entries.length < 2) { + return entries[0] || null + } + + const sorted = entries.sort() + const second = entries[1] + let previous = second + return ( + sorted.reduce((acc, value) => { + const a = value + const b = acc || previous + let i = 0 + while (i < a.length && a.charAt(i) === b.charAt(i)) { + i++ + } + return a.substring(0, i) + }) || null + ) +} + +export type JavaCompileResultBase = { + buildroot: string + result: CompileResult + classes: string[] + resources: string[] + outputs: string[] +} + +export type JavaCompileResult = + | (JavaCompileResultBase & { + isMultiModular: true + module: string + }) + | (JavaCompileResultBase & { + isMultiModular: false + }) + +export type CompilePackageResult = JavaCompileResult & { + jar: string +} + +function replaceInArg(tmpdir: string, arg: string): string { + return arg.replaceAll('$(tmpdir)', tmpdir) +} + +function maybeTrimCwd(options: CompileJavaOptions | null, cwd: string, arg: string): string { + if (options?.preserveArgPaths === true) { + return arg + } + if (arg.startsWith(cwd)) { + return arg.substring(cwd.length + 1) + } + if (arg.includes(`=${cwd}`)) { + return arg.replace(`=${cwd}`, '=').replace('=/', '=') + } + return arg +} + +export async function compileJava( + sources: (string | { from: string; to: string })[], + options?: CompileJavaOptions +): Promise { + // create temporary directory where the compile will be run + const tmpdir = await testTmpdir() + + // detect a multi-module-capable modular build, which will need the module name + const args = options?.args || [] + const isMultiModular: boolean = !!args.find(arg => { + return arg.startsWith('--module') || arg.startsWith('--module-source-path') + }) + let argI = 0 + let moduleName: string | null = null + for (const arg of args) { + if (arg.startsWith('--module=')) { + moduleName = arg.split('=')[1] + break + } else if (arg === '--module') { + // get the next arg + moduleName = args[argI + 1] + break + } + argI++ + } + + // if we're in a multi-modular build, and we have the module name, setup the source path to use it + let srcroot = tmpdirPath(tmpdir, 'src') + if (isMultiModular && moduleName) { + compileDebug(`multi-modular build detected, setting srcroot to: ${srcroot}`) + srcroot = tmpdirPath(tmpdir, join('src', moduleName)) + } else { + compileDebug(`using srcroot: ${srcroot}`) + } + + // if no explicit prefix is set, try to identify one from the sources, by looking for a common maximum + // substring shared by all entries. + const sourcefiles = sources.map(s => (typeof s === 'string' ? s : s.from)).filter(s => s.endsWith('.java')) + compileDebug(`gathered source files: ${sourcefiles.length} java`) + const commonPrefix = sourcefiles.length > 1 ? options?.trimPath || longestCommonSubstring(sourcefiles) || '' : '' + + // copy source files + const buildroot = tmpdirPath(tmpdir, 'build') + compileDebug(`making buildroot: ${buildroot}`) + await mkdir(buildroot) + compileDebug(`making srcroot: ${srcroot}`) + await mkdir(srcroot, { recursive: true }) + const javaSources: string[] = [] + + // process sources into source root, filter resources as we go + for (const source of sources) { + const srcpathRelative = typeof source === 'string' ? source : source.from + const topathRelative = maybeTrimSourcePath( + commonPrefix, + typeof source === 'string' ? source : source.to || source.from + ) + const isResource = !srcpathRelative.endsWith('.java') + + const srcpath = tmpdirPath(srcroot, srcpathRelative) + const topath = isAbsolute(topathRelative) ? topathRelative : tmpdirPath(srcroot, topathRelative) + compileDebug(`copying src: ${srcpathRelative} → ${topathRelative}`) + + const readPath = isAbsolute(srcpathRelative) ? srcpathRelative : mockPath(`compiler/${srcpathRelative}`) + const src = readFileSync(readPath, 'utf-8') + const srcparent = dirname(topath) + if (!existsSync(srcparent)) { + compileDebug(`making src parent: ${srcparent}`) + await mkdir(srcparent, { recursive: true }) + } + + writeFileSync(topath, src) + expect(existsSync(topath)).toBe(true) + const classpath = classpathForSourcePath(srcpath) + expect(existsSync(classpath)).toBe(false) + + if (isResource) { + // copy the resource to the build path proactively so it can be seen by tools like `jar` + const buildResource = tmpdirPath(buildroot, topathRelative) + compileDebug(`copying resource to build: ${buildResource}`) + const parent = dirname(buildResource) + if (!existsSync(parent)) { + compileDebug(`making build parent: ${parent}`) + await mkdir(parent, { recursive: true }) + } + writeFileSync(buildResource, src) + expect(existsSync(buildResource)).toBe(true) + } else { + javaSources.push(topathRelative) + } + } + + // run the compile + compileDebug(`obtaining compiler`) + const javac = currentToolchain().compiler() + javac.logs(false).cwd(tmpdir) + + const effectiveArgs = [ + ...javaSources.map(s => (isAbsolute(s) ? s : tmpdirPath(srcroot, s))), + '-d', + buildroot, + ...(options?.args ?? []) + ].map(arg => maybeTrimCwd(options || null, tmpdir, replaceInArg(tmpdir, arg))) + + compileDebug(`compiling: ${effectiveArgs.join(' ')}`) + const result = await javac.compile(effectiveArgs) + compileDebug(`compile result: ${JSON.stringify(result, null, ' ')}`) + + // collect the class files and return + const classes = glob.sync(tmpdirPath(buildroot, '**/*.class')) + const resources = glob.sync(tmpdirPath(buildroot, '**/*'), { nodir: true, ignore: '**/*.class' }) + + // combine and sort to provide all outputs + const outputs = [...classes, ...resources].sort() + + const out = { + buildroot, + result, + classes, + resources, + outputs, + isMultiModular, + module: isMultiModular ? moduleName : undefined + } + expect(out).toBeDefined() + expect(out.buildroot).toBeDefined() + expect(out.classes).toBeDefined() + expect(out.resources).toBeDefined() + expect(out.outputs).toBeDefined() + expect(out.result).toBeDefined() + expect(out.result.run.exitCode).toBe(0) + return out as JavaCompileResult +} + +export async function compileAndPackageJar( + sources: (string | { from: string; to: string })[], + manifest: string[] = [], + options?: CompilePackageJarOptions +): Promise { + // compile the sources; this also copies resources + const result = await compileJava(sources, options) + + // obtain the build path so we can write the manifest + const buildroot = result.buildroot + const manifestPath = join(dirname(result.buildroot), 'MANIFEST.MF') + + // account for main class in manifest + if (options?.mainClass && !manifest.find(m => m.startsWith('Main-Class'))) { + manifest.push(`Main-Class: ${options.mainClass}`) + } + + // render and write the manifest + const renderedManifest = manifestDefaultLines.concat(manifest).concat(['']).join('\n') + const jarName = options?.jarName ?? 'out.jar' + compileDebug(`writing jar manifest → ${manifestPath}`) + writeFileSync(manifestPath, renderedManifest) + + // obtain the jar tool to use + const jar = currentToolchain().tool('jar') + jar.logs(false).cwd(buildroot) + + const jarArgs = ['--create', `--file=${jarName}`, `--manifest=${manifestPath}`] + + if (options?.modulePath) { + jarArgs.push(`--module-path=${options.modulePath}`) + } + if (options?.moduleVersion) { + jarArgs.push(`--module-version=${options.moduleVersion}`) + } + if (options?.hashModules) { + jarArgs.push(`--hash-modules=${options.hashModules}`) + } + if (options?.mainClass) { + jarArgs.push(`--main-class=${options.mainClass}`) + } + if (options?.jarArgs) { + jarArgs.push(...options.jarArgs) + } + jarArgs.push('.') + + compileDebug(`running jar: ${jarArgs.join(' ')}`) + const callResult = await jar.run(jarArgs) + expect(callResult).toBeDefined() + expect(callResult.exitCode).toBe(0) + + // build the jar + return { + ...result, + jar: join(buildroot, jarName) + } +} + +test('test util: should find a basic common substring', () => { + const result = longestCommonSubstring(['abc', 'abd']) + expect(result).toBe('ab') +}) + +test('test util: should find a basic path substring', () => { + const result = longestCommonSubstring(['testing/hello/one/two/three.txt', 'testing/again.txt']) + expect(result).toBe('testing/') +}) + +test('test util: should not die when there is no common substring', () => { + const result = longestCommonSubstring(['hello/one/two/three.txt', 'testing/again.txt']) + expect(result).toBe(null) +}) diff --git a/packages/java/toolchain/abstract.ts b/packages/java/toolchain/abstract.ts index 7503004d..4f38cc5b 100644 --- a/packages/java/toolchain/abstract.ts +++ b/packages/java/toolchain/abstract.ts @@ -11,7 +11,131 @@ * License for the specific language governing permissions and limitations under the License. */ -import { JavaToolchain } from "../javahome"; +import { spawn, SpawnOptions } from 'node:child_process' +import { join, normalize } from 'node:path' +import { JavaToolchain } from '../java-home' +import { Readable } from 'node:stream' + +/** + * Information about a resolved tool binary. + */ +export type BinInfo = { + relative: string + absolute: string + name: string +} + +/** + * Arguments for a run of a toolchain tool. + */ +export type ToolArgs = string[] + +/** + * Environment for a run of a toolchain tool. + */ +export type ToolEnv = Record + +/** + * Execution spec for running a tool. + */ +export type ExecSpec = { + bin: BinInfo + args: ToolArgs + options?: SpawnOptions +} + +/** + * Result of the run of a toolchain tool. + */ +export type ToolRun = { + start: number + finish: number + duration: number + exitCode: number + exec: ExecSpec + env: ToolEnv + cwd: string + result: { stdout: string; stderr: string } +} + +const defaultSpawnOptions: Partial = { + cwd: '.', + stdio: 'pipe', + detached: false, + shell: false, + windowsHide: true, + timeout: 30_000, + killSignal: 'SIGTERM' +} + +// Buffer a stream into a string. +async function bufferStream(stream: Readable): Promise { + const chunks: Buffer[] = [] + return new Promise((resolve, reject) => { + stream.on('data', chunk => chunks.push(Buffer.from(chunk))) + stream.on('error', err => reject(err)) + stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))) + }) +} + +/** + * Tool Error + * + * Describes an error that occurred while running a tool; the exit code is used to generate a message, and the stdout + * and stderr strings for the run are made available. + * + * Call the `explain` method to produce a string block about the error, suitable for printing via `console.error`. + */ +export class ToolError extends Error { + private constructor( + public readonly run: ToolRun, + message?: string + ) { + super(message || ToolError.explainErr(run)) + } + + // Create a new error from a run. + static fromRun(run: ToolRun, message?: string): ToolError { + return new ToolError(run, message) + } + + /** + * Explain this error in a string block + * + * @returns A string block about the error, suitable for printing via `console.error` + */ + explain(): string { + return ToolError.explainErr(this.run) + } + + private static explainErr(run: ToolRun): string { + return [ + `${run.exec.bin.name} failed with exit code ${run.exitCode} +================================================================================= +Working Directory: + ${run.cwd} + +Command line: + ${run.exec.bin.absolute} \\ +${run.exec.args.map(it => ` ${it}`).join(' \\ \n')} + +Environment: +--------------------------------------------------------------------------------- +${Object.entries(run.env || {}) + .map(([k, v]) => `- ${k} = ${v}`) + .join('\n')} +`, + ` +Standard Error: +--------------------------------------------------------------------------------- +${run.result.stderr} +================================================================================= +` + ] + .map(msg => msg.trim()) + .join('\n\n') + } +} /** * Abstract Java Tool @@ -19,6 +143,18 @@ import { JavaToolchain } from "../javahome"; * Provides shared baseline implmentation logic for a Java Toolchain-provided tool. */ export abstract class JavaTool { + // Current working directory for this tool. + private _cwd: string = process.cwd() + + // Whether logs should be emitted to the console. + private _logs: boolean = true + + // All runs seen by this instance. + private readonly runs: ToolRun[] = [] + + // Abort controller for the current run. + private readonly abortController = new AbortController() + /** * Protected constructor. @@ -26,4 +162,173 @@ export abstract class JavaTool { */ // @ts-ignore protected constructor(protected readonly toolchain: JavaToolchain) {} + + /** + * Perform the underlying execution for a tool run + * + * @param spec Specification for the execution + * @returns Return value of `spawnSync` with content as a buffer + */ + private async execTool(spec: ExecSpec): Promise { + // initial pre-work + await this.onPreExec(spec) + const env = this.env(spec.args) + const cwd = this._cwd + const options = { + ...defaultSpawnOptions, + signal: this.abortController.signal, + cwd, + env, + ...(spec.options || {}) + } + const start = performance.now() + + // spawn and run + const exec: Promise = new Promise((accept, reject) => { + const tool = spawn(spec.bin.absolute, spec.args, options) + const { stdout, stderr } = tool + if (!tool || !tool.pid || !stdout || !stderr) { + reject(new Error('Failed to spawn tool')) + return + } + + const stdoutPromise: Promise = bufferStream(stdout) + const stderrPromise: Promise = bufferStream(stderr) + + tool.on('close', async code => { + const finish = performance.now() + const run = { + exitCode: code === 0 ? 0 : code || -1, + exec: spec, + env, + cwd, + start, + finish, + duration: finish - start, + result: { + stdout: await stdoutPromise, + stderr: await stderrPromise + } + } + if (code === 0) { + accept(run) + } else { + // handle errors + reject(ToolError.fromRun(run)) + } + }) + }) + + // cleanup and finish + try { + const result = await exec + this.runs.push(result) + return result + } catch (err) { + if (err instanceof ToolError) { + this.runs.push(err.run) + this.err(err) + } + throw err + } finally { + await this.onPostExec() + } + } + + /** + * Resolve information about a named binary from the target toolchain. + * + * @param name Name of the binary to resolve info for (like `javac`) + * @return Information about the binary + */ + protected bin(name: string): BinInfo { + const target = normalize(join(this.toolchain.path(), 'bin', name)) + return { + name, + relative: join('bin', name), + absolute: target + } + } + + /** + * Handle tooling execution errors by printing to the console (by default) + * + * @param err Error that was experienced when running the tool + */ + protected err(err: ToolError) { + if (this._logs) { + console.error(err.explain()) + } + } + + /** + * Produce environment variables to use for a tool execution + * + * @param _args Arguments for this execution of the tool + * @returns Environment to use for the execution + */ + protected env(_args: ToolArgs): ToolEnv { + return { + PWD: process.env.PWD || process.cwd(), + PATH: process.env.PATH || '', + JAVA_HOME: this.toolchain.path() + } + } + + /** + * Execute pre-run actions, like creating temporary directories or setting up configuration + * + * @param _spec Spec for the execution to be run + */ + protected async onPreExec(_spec: ExecSpec) { + // Nothing, by default. + } + + /** + * Execute post-run actions, like cleaning up or verifying results + */ + protected async onPostExec() { + // Nothing, by default. + } + + /** + * Produce an execution spec based on the current tool's usage. + * + * @param args Arguments passed for this execution + */ + protected abstract exec(args: ToolArgs): ExecSpec + + /** + * Run the underlying tool with the provided arguments + * + * @param args Arguments to run the tool with + * @returns Result of the tool run + */ + protected async invoke(args: ToolArgs): Promise { + return this.execTool(this.exec(args)) + } + + // --- Public API + + /** + * Change the current-working-directory for this tool + * + * @param path Path to change the CWD to for this execution + * @returns This tool, for chaining + */ + cwd(path: string): JavaTool { + this._cwd = path + return this + } + + /** + * Enable or disable log messages + * + * @param enabled Whether to enable console log messages + * @returns This tool, for chaining + */ + logs(enabled: boolean): JavaTool { + this._logs = enabled + return this + } } diff --git a/packages/java/toolchain/compiler.ts b/packages/java/toolchain/compiler.ts index d9a845e6..f56b4b1e 100644 --- a/packages/java/toolchain/compiler.ts +++ b/packages/java/toolchain/compiler.ts @@ -11,15 +11,30 @@ * License for the specific language governing permissions and limitations under the License. */ -import { JavaTool } from "./abstract"; -import type { JavaToolchain } from "../javahome"; +import { BinInfo, ExecSpec, JavaTool, ToolArgs, ToolRun } from './abstract' +import type { JavaToolchain } from '../java-home' + +/** + * Name of the Java compiler binary. + */ +const COMPILER_BINARY_NAME = 'javac' + +/** + * Structure of a compiler return result. + */ +export type CompileResult = { + run: ToolRun +} /** * Java Compiler */ export class JavaCompiler extends JavaTool { + private readonly _bin: BinInfo + private constructor(toolchain: JavaToolchain) { - super(toolchain); + super(toolchain) + this._bin = this.bin(COMPILER_BINARY_NAME) } /** @@ -30,8 +45,26 @@ export class JavaCompiler extends JavaTool { */ // @ts-ignore static forToolchain(toolchain: JavaToolchain): JavaCompiler { - throw new Error("not yet implemented"); + return new JavaCompiler(toolchain) + } + + // Execute the compiler with the provided arguments. + protected override exec(args: ToolArgs): ExecSpec { + return { + bin: this._bin, + args + } + } + + /** + * Run the compiler with the provided arguments, producing a structured result + * + * @param args Arguments to pass to the compiler + */ + async compile(args: ToolArgs): Promise { + const run = await this.invoke(args) + return { run } } } -export default JavaCompiler; +export default JavaCompiler diff --git a/packages/java/toolchain/index.ts b/packages/java/toolchain/index.ts index 31f45a5c..a98eb2ef 100644 --- a/packages/java/toolchain/index.ts +++ b/packages/java/toolchain/index.ts @@ -11,5 +11,6 @@ * License for the specific language governing permissions and limitations under the License. */ -export { JavaCompiler } from "./compiler"; -export { JavaLauncher } from "./launcher"; +export { JavaCompiler } from './compiler' +export { JavaLauncher } from './launcher' +export { JdkTool } from './tool' diff --git a/packages/java/toolchain/launcher.ts b/packages/java/toolchain/launcher.ts index 50f4c737..1b369ee6 100644 --- a/packages/java/toolchain/launcher.ts +++ b/packages/java/toolchain/launcher.ts @@ -11,15 +11,30 @@ * License for the specific language governing permissions and limitations under the License. */ -import { JavaTool } from "./abstract"; -import type { JavaToolchain } from "../javahome"; +import { BinInfo, ExecSpec, JavaTool, ToolArgs, ToolRun } from './abstract' +import type { JavaToolchain } from '../java-home' + +/** + * Name of the Java launcher binary. + */ +const LAUNCHER_BINARY_NAME = 'java' + +/** + * Structure of a return result from a run of the Java launcher. + */ +export type LauncherResult = { + run: ToolRun +} /** * Java Launcher */ export class JavaLauncher extends JavaTool { + private readonly _bin: BinInfo + private constructor(toolchain: JavaToolchain) { - super(toolchain); + super(toolchain) + this._bin = this.bin(LAUNCHER_BINARY_NAME) } /** @@ -30,8 +45,26 @@ export class JavaLauncher extends JavaTool { */ // @ts-ignore static forToolchain(toolchain: JavaToolchain): JavaLauncher { - throw new Error("not yet implemented"); + return new JavaLauncher(toolchain) + } + + // Execute the Java launcher with the provided arguments. + protected override exec(args: ToolArgs): ExecSpec { + return { + bin: this._bin, + args + } + } + + /** + * Run the compiler with the provided arguments, producing a structured result + * + * @param args Arguments to pass to the compiler + */ + async launch(args: ToolArgs): Promise { + const run = await this.invoke(args) + return { run } } } -export default JavaLauncher; +export default JavaLauncher diff --git a/packages/java/toolchain/repositories.ts b/packages/java/toolchain/repositories.ts new file mode 100644 index 00000000..e4cf00de --- /dev/null +++ b/packages/java/toolchain/repositories.ts @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2024 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under the License. + */ + +import { JvmPlatform, JvmTarget, JavaToolchainVendor } from '../java-model' + +type LatestVersionTag = { + latest: string +} + +type ToolchainTargetInfo = LatestVersionTag & { + platforms: { + [platform in JvmPlatform]: URL + } +} + +type ToolchainData = { + [key: string]: { + [key: string]: ToolchainTargetInfo + } +} + +function url(s: string): URL { + return new URL(s) +} + +/** + * Toolchain Repositories + * + * Mapping of Java toolchain downloads by vendor, version, and platform. + */ +export const ToolchainRepositories: ToolchainData = { + [`${JavaToolchainVendor.GRAALVM}`]: { + [`${JvmTarget.JDK_22}`]: { + latest: '22.0.0', + platforms: { + [`${JvmPlatform.LINUX_AMD64}`]: url( + 'https://download.oracle.com/graalvm/22/latest/graalvm-jdk-22_linux-x64_bin.tar.gz' + ), + [`${JvmPlatform.LINUX_AARCH64}`]: url( + 'https://download.oracle.com/graalvm/22/latest/graalvm-jdk-22_linux-aarch64_bin.tar.gz' + ), + [`${JvmPlatform.DARWIN_AMD64}`]: url( + 'https://download.oracle.com/graalvm/22/latest/graalvm-jdk-22_macos-x64_bin.tar.gz' + ), + [`${JvmPlatform.DARWIN_AARCH64}`]: url( + 'https://download.oracle.com/graalvm/22/latest/graalvm-jdk-22_macos-aarch64_bin.tar.gz' + ), + [`${JvmPlatform.WINDOWS_AMD64}`]: url( + 'https://download.oracle.com/graalvm/22/latest/graalvm-jdk-22_windows-x64_bin.zip' + ) + } + }, + [`${JvmTarget.JDK_21}`]: { + latest: '21.0.2', + platforms: { + [`${JvmPlatform.LINUX_AMD64}`]: url( + 'https://download.oracle.com/graalvm/21/latest/graalvm-jdk-21_linux-x64_bin.tar.gz' + ), + [`${JvmPlatform.LINUX_AARCH64}`]: url( + 'https://download.oracle.com/graalvm/21/latest/graalvm-jdk-21_linux-aarch64_bin.tar.gz' + ), + [`${JvmPlatform.DARWIN_AMD64}`]: url( + 'https://download.oracle.com/graalvm/21/latest/graalvm-jdk-21_macos-x64_bin.tar.gz' + ), + [`${JvmPlatform.DARWIN_AARCH64}`]: url( + 'https://download.oracle.com/graalvm/21/latest/graalvm-jdk-21_macos-aarch64_bin.tar.gz' + ), + [`${JvmPlatform.WINDOWS_AMD64}`]: url( + 'https://download.oracle.com/graalvm/21/latest/graalvm-jdk-21_windows-x64_bin.zip' + ) + } + }, + [`${JvmTarget.JDK_17}`]: { + latest: '17.0.10', + platforms: { + [`${JvmPlatform.LINUX_AMD64}`]: url( + 'https://download.oracle.com/graalvm/17/latest/graalvm-jdk-17_linux-x64_bin.tar.gz' + ), + [`${JvmPlatform.LINUX_AARCH64}`]: url( + 'https://download.oracle.com/graalvm/17/latest/graalvm-jdk-17_linux-aarch64_bin.tar.gz' + ), + [`${JvmPlatform.DARWIN_AMD64}`]: url( + 'https://download.oracle.com/graalvm/17/latest/graalvm-jdk-17_macos-x64_bin.tar.gz' + ), + [`${JvmPlatform.DARWIN_AARCH64}`]: url( + 'https://download.oracle.com/graalvm/17/latest/graalvm-jdk-17_macos-aarch64_bin.tar.gz' + ), + [`${JvmPlatform.WINDOWS_AMD64}`]: url( + 'https://download.oracle.com/graalvm/17/latest/graalvm-jdk-17_windows-x64_bin.zip' + ) + } + } + } +} + +/** + * Get the repository for a given toolchain + * + * @param vendor Toolchain vendor + * @param version Toolchain version + * @param platform Toolchain platform + * @returns The repository for the given toolchain + * @throws If the vendor, version, or platform is unknown to the toolchain manager + */ +export function repositoryForToolchain(vendor: JavaToolchainVendor, version: JvmTarget, platform: JvmPlatform): URL { + const targetVendor = ToolchainRepositories[vendor] + if (!targetVendor) throw new Error(`Unknown vendor: ${vendor}`) + const targetVersion = targetVendor[version] + if (!targetVersion) throw new Error(`Unknown version: ${version}`) + const targetPlatform = targetVersion.platforms[platform] + if (!targetPlatform) throw new Error(`Unknown platform: ${platform}`) + return targetPlatform +} diff --git a/packages/java/toolchain/tool.ts b/packages/java/toolchain/tool.ts new file mode 100644 index 00000000..75bc415f --- /dev/null +++ b/packages/java/toolchain/tool.ts @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under the License. + */ + +import { BinInfo, ExecSpec, JavaTool, ToolArgs, ToolRun } from './abstract' +import type { JavaToolchain } from '../java-home' + +/** + * Generic JDK Tool + */ +export class JdkTool extends JavaTool { + private readonly _bin: BinInfo + + private constructor(toolchain: JavaToolchain, tool: string) { + super(toolchain) + this._bin = this.bin(tool) + } + + /** + * Wrap the provided toolchain for use with an arbitrary binary + * + * @param toolchain Toolchain to wrap + * @param tool Tool to wrap + * @return Tool wrapper + */ + // @ts-ignore + static forToolchain(toolchain: JavaToolchain, tool: string): JdkTool { + return new JdkTool(toolchain, tool) + } + + // Execute the binary with the provided arguments. + protected override exec(args: ToolArgs): ExecSpec { + return { + bin: this._bin, + args + } + } + + /** + * Run the tool with the provided arguments, producing a structured result + * + * @param args Arguments to pass to the tool + */ + async run(args: ToolArgs): Promise { + return await this.invoke(args) + } +} + +export default JdkTool diff --git a/packages/maven/README.md b/packages/maven/README.md index 6908d522..69f89f48 100644 --- a/packages/maven/README.md +++ b/packages/maven/README.md @@ -1,10 +1,12 @@ # Java Tools: Maven -This package provides JavaScript logic for working with [Maven projects][0], [POM files][1], and other stuff generally related to Maven. +This package provides JavaScript logic for working with [Maven projects][0], [POM files][1], and other stuff generally +related to Maven. ## About this Project -Part of the _[Java Modules](https://javamodules.dev)_ project; licensed as MIT. Contributions and issues are [welcome][1]. +Part of the _[Java Modules](https://javamodules.dev)_ project; licensed as MIT. Contributions and issues are +[welcome][1]. [0]: https://maven.apache.org/ [1]: https://maven.apache.org/pom.html diff --git a/packages/maven/index.mts b/packages/maven/index.mts index c73472eb..d694cdad 100644 --- a/packages/maven/index.mts +++ b/packages/maven/index.mts @@ -11,4 +11,4 @@ * License for the specific language governing permissions and limitations under the License. */ -export * from "./maven-model"; +export * from './maven-model' diff --git a/packages/maven/maven-constants.ts b/packages/maven/maven-constants.ts index 457c4701..a411d0c3 100644 --- a/packages/maven/maven-constants.ts +++ b/packages/maven/maven-constants.ts @@ -14,4 +14,4 @@ /** * Expected Project Object Model (POM) version. */ -export const POM_MODEL_VERSION = "4.0.0"; +export const POM_MODEL_VERSION = '4.0.0' diff --git a/packages/maven/maven-model.ts b/packages/maven/maven-model.ts index 5d9ed1f2..30743a99 100644 --- a/packages/maven/maven-model.ts +++ b/packages/maven/maven-model.ts @@ -11,7 +11,7 @@ * License for the specific language governing permissions and limitations under the License. */ -const MAVEN_SEPARATOR = ":"; +const MAVEN_SEPARATOR = ':' /** * Maven Coordinate @@ -19,11 +19,11 @@ const MAVEN_SEPARATOR = ":"; * Describes the shape of a parsed Maven coordinate, with a group, artifact, and version */ export type MavenCoordinate = { - groupId: string; - artifactId: string; - version?: string; - classifier?: string; -}; + groupId: string + artifactId: string + version?: string + classifier?: string +} /** * Maven Project: Coordinate @@ -31,10 +31,10 @@ export type MavenCoordinate = { * Specializes a Maven Coordinate for the raw structure provided by a POM */ export type MavenProjectCoordinate = { - groupId: string; - artifactId: string; - version: string; -}; + groupId: string + artifactId: string + version: string +} /** * Maven Project: Parent Coordinate @@ -44,8 +44,8 @@ export type MavenProjectCoordinate = { export type MavenProjectParentCoordinate = | MavenProjectCoordinate | { - relativePath?: string; - }; + relativePath?: string + } /** * Maven Project: Parent Info @@ -56,8 +56,8 @@ export type MavenProjectParentCoordinate = * If provided, the parent coordinate version is required and an optional relative path can be specified. */ export type MavenProjectParentInfo = { - parent: MavenProjectParentCoordinate; -}; + parent: MavenProjectParentCoordinate +} /** * Maven Project: Coordinate or Inheritance @@ -68,7 +68,7 @@ export type MavenProjectParentInfo = { * * Note that it is possible to provide these properties as well as a parent. */ -export type MavenProjectCoordinateOrInherited = MavenProjectCoordinate | MavenProjectParentInfo; +export type MavenProjectCoordinateOrInherited = MavenProjectCoordinate | MavenProjectParentInfo /** * Maven Project: Packaging @@ -77,10 +77,10 @@ export type MavenProjectCoordinateOrInherited = MavenProjectCoordinate | MavenPr * be extended, so `string` is also accepted in the POM object. */ export enum MavenProjectPackaging { - POM = "pom", - JAR = "jar", - WAR = "war", - BUNDLE = "bundle", + POM = 'pom', + JAR = 'jar', + WAR = 'war', + BUNDLE = 'bundle' } /** @@ -89,7 +89,7 @@ export enum MavenProjectPackaging { * Enumerates known distribution types for a license which is mapped in the `` block for a Maven project. */ export enum MavenProjectLicenseDistribution { - REPO = "repo", + REPO = 'repo' } /** @@ -98,10 +98,10 @@ export enum MavenProjectLicenseDistribution { * Describes a license which is mapped in the `` block for a Maven proejct. */ export type MavenProjectLicense = { - name: string; - url?: string; - distribution?: MavenProjectLicenseDistribution | string; -}; + name: string + url?: string + distribution?: MavenProjectLicenseDistribution | string +} /** * Maven Project: Contact Person @@ -109,10 +109,10 @@ export type MavenProjectLicense = { * Describes a contact (of type person) which is listed in a Maven project. */ export type MavenProjectContactPerson = { - name: string; - email?: string; - url?: string; -}; + name: string + email?: string + url?: string +} /** * Maven Project: Contact Organization @@ -120,9 +120,9 @@ export type MavenProjectContactPerson = { * Describes a contact (of type organization) which is listed in a Maven project. */ export type MavenProjectContactOrg = { - organization: string; - organizationUrl?: string; -}; + organization: string + organizationUrl?: string +} /** * Maven Project: Contact @@ -132,8 +132,8 @@ export type MavenProjectContactOrg = { */ export type MavenProjectContact = Partial & Partial & { - id: string; - }; + id: string + } /** * Maven Project: Source Control @@ -141,10 +141,10 @@ export type MavenProjectContact = Partial & * Describes information specified in the `` or `` fields within a Maven project. */ export type MavenProjectSourceControl = { - url: string; - connection: string; - developerConnection?: string; -}; + url: string + connection: string + developerConnection?: string +} /** * Maven Project: Dependency Use @@ -152,9 +152,9 @@ export type MavenProjectSourceControl = { * Enumerates types of dependency usage which can be specified for a given Maven dependency. */ export enum MavenProjectDependencyUse { - COMPILE = "compile", - PROVIDED = "provided", - TEST = "test", + COMPILE = 'compile', + PROVIDED = 'provided', + TEST = 'test' } /** @@ -164,15 +164,15 @@ export enum MavenProjectDependencyUse { * `scope` property. */ export type MavenProjectDependencyScope = { - scope: MavenProjectDependencyUse | string; -}; + scope: MavenProjectDependencyUse | string +} /** * Maven Project: Dependency Exclusion * * Describes exclusions for a dependency mapped within a Maven project. */ -export type MavenProjectDependencyExclusion = Omit; +export type MavenProjectDependencyExclusion = Omit /** * Maven Project: Dependency Exclusions @@ -180,8 +180,8 @@ export type MavenProjectDependencyExclusion = Omit; * Describes exclusions for a dependency mapped within a Maven project. */ export type MavenProjectDependencyExclusions = { - exclusions: MavenProjectDependencyExclusion[]; -}; + exclusions: MavenProjectDependencyExclusion[] +} /** * Maven Project: Dependency @@ -191,7 +191,7 @@ export type MavenProjectDependencyExclusions = { */ export type MavenProjectDependency = MavenCoordinate & Partial & - Partial; + Partial /** * Maven Project: Managed Dependency @@ -199,7 +199,7 @@ export type MavenProjectDependency = MavenCoordinate & * Describes a dependency mapped in the `` block; a managed dependency can include exclusion * information, but no scope. */ -export type MavenProjectManagedDependency = MavenCoordinate & Partial; +export type MavenProjectManagedDependency = MavenCoordinate & Partial /** * Maven Project @@ -209,17 +209,17 @@ export type MavenProjectManagedDependency = MavenCoordinate & Partial & { export type MavenProject = { - modelVersion: string; - name?: string; - description?: string; - url?: string; - packaging: MavenProjectPackaging | string; - licenses?: MavenProjectLicense[]; - developers?: MavenProjectContact[]; - scm?: MavenProjectSourceControl; - dependencyManagement?: MavenProjectManagedDependency[]; - dependencies?: MavenProjectDependency[]; -}; + modelVersion: string + name?: string + description?: string + url?: string + packaging: MavenProjectPackaging | string + licenses?: MavenProjectLicense[] + developers?: MavenProjectContact[] + scm?: MavenProjectSourceControl + dependencyManagement?: MavenProjectManagedDependency[] + dependencies?: MavenProjectDependency[] +} /** * Parse a Maven coordinate from a string @@ -230,14 +230,14 @@ export type MavenProject = { * @returns Maven Coordinate record */ export function parseMavenCoordinate(coordinate: string): MavenCoordinate { - const segments = coordinate.split(MAVEN_SEPARATOR); + const segments = coordinate.split(MAVEN_SEPARATOR) // requires at least 3 segments (`group:artifact:version`) - if (segments.length < 3) throw new Error(`Invalid segment count in Maven coordinate: '${coordinate}'`); - const version = segments[segments.length - 1]; // version is last - const artifact = segments[segments.length - 2]; // artifact is second-to-last - const group = segments.slice(0, segments.length - 2).join(MAVEN_SEPARATOR); - return mavenCoordinate(group, artifact, version); + if (segments.length < 3) throw new Error(`Invalid segment count in Maven coordinate: '${coordinate}'`) + const version = segments[segments.length - 1] // version is last + const artifact = segments[segments.length - 2] // artifact is second-to-last + const group = segments.slice(0, segments.length - 2).join(MAVEN_SEPARATOR) + return mavenCoordinate(group, artifact, version) } /** @@ -252,7 +252,7 @@ export function mavenCoordinate( groupId: string, artifactId: string, version?: string, - classifier?: string, + classifier?: string ): MavenCoordinate { return { groupId, @@ -260,6 +260,6 @@ export function mavenCoordinate( version, classifier, // @ts-ignore - valueOf: () => `${groupId}:${artifactId}:${version}`, - }; + valueOf: () => `${groupId}:${artifactId}:${version}` + } } diff --git a/packages/maven/maven-parser.ts b/packages/maven/maven-parser.ts index f51db54c..b828d579 100644 --- a/packages/maven/maven-parser.ts +++ b/packages/maven/maven-parser.ts @@ -41,48 +41,48 @@ * both MIT, with the original under Intuit's ownership, and extensions under Elide's ownership. */ -import fs from "node:fs"; -import xml2js, { Options as XmlParseOptions } from "xml2js"; -import traverse from "traverse"; +import fs from 'node:fs' +import xml2js, { Options as XmlParseOptions } from 'xml2js' +import traverse from 'traverse' -export type { XmlParseOptions }; +export type { XmlParseOptions } // xmljs options https://github.com/Leonidas-from-XIV/node-xml2js#options let XML2JS_OPTS = { trim: true, normalizeTags: true, normalize: true, - mergeAttrs: true, -}; + mergeAttrs: true +} -export type PomProject = Record; +export type PomProject = Record export type PomObject = { - project: PomProject; + project: PomProject } & { - [key: string]: string; -}; + [key: string]: string +} export interface ParsedOutput { - pomXml: string; - pomObject: PomObject; - xmlContent?: string; + pomXml: string + pomObject: PomObject + xmlContent?: string } /** * Options for the parser, with XML parsing options available as well. */ -export type ParseOptions = (XmlParseOptions & { filePath?: string; xmlContent?: string }) | null; +export type ParseOptions = (XmlParseOptions & { filePath?: string; xmlContent?: string }) | null /** * Callback type for the parser. */ -export type ParseCallback = (err: Error | null, result?: ParsedOutput | null) => void; +export type ParseCallback = (err: Error | null, result?: ParsedOutput | null) => void const checkEmpty = (value: string | null | undefined): string => { - if (!!value) return value; - throw new Error("Cannot parse empty or invalid XML"); -}; + if (!!value) return value + throw new Error('Cannot parse empty or invalid XML') +} /** * This method exposes an `async/await` syntax to the older `parse` method and allows you to call the method with just @@ -94,20 +94,20 @@ const checkEmpty = (value: string | null | undefined): string => { export async function parseAsync(opt: ParseOptions): Promise { if (!opt) throw new Error( - "You must provide options: opt.filePath and any other option of " + - "https://github.com/Leonidas-from-XIV/node-xml2js#options", - ); - if (!opt.xmlContent && !opt.filePath) throw new Error("You must provide the opt.filePath or the opt.xmlContent"); + 'You must provide options: opt.filePath and any other option of ' + + 'https://github.com/Leonidas-from-XIV/node-xml2js#options' + ) + if (!opt.xmlContent && !opt.filePath) throw new Error('You must provide the opt.filePath or the opt.xmlContent') if (opt.filePath) { - const xmlContent = checkEmpty(await readFileAsync(opt.filePath, "utf8")); - const result = await _parseWithXml2js(xmlContent); - return result; + const xmlContent = checkEmpty(await readFileAsync(opt.filePath, 'utf8')) + const result = await _parseWithXml2js(xmlContent) + return result } - const result = await _parseWithXml2js(checkEmpty(opt.xmlContent || "")); - delete result.xmlContent; - return result; + const result = await _parseWithXml2js(checkEmpty(opt.xmlContent || '')) + delete result.xmlContent + return result } /** @@ -118,38 +118,38 @@ export async function parseAsync(opt: ParseOptions): Promise { function parse(opt: ParseOptions, callback: ParseCallback): void { if (!opt) throw new Error( - "You must provide options: opt.filePath and any other option of " + - "https://github.com/Leonidas-from-XIV/node-xml2js#options", - ); + 'You must provide options: opt.filePath and any other option of ' + + 'https://github.com/Leonidas-from-XIV/node-xml2js#options' + ) - if (!opt.xmlContent && !opt.filePath) throw new Error("You must provide the opt.filePath or the opt.xmlContent"); + if (!opt.xmlContent && !opt.filePath) throw new Error('You must provide the opt.filePath or the opt.xmlContent') // If the xml content is was not provided by the api client. // https://github.com/petkaantonov/bluebird/blob/master/API.md#error-rejectedhandler----promise if (opt.filePath) { - readFileAsync(opt.filePath, "utf8") + readFileAsync(opt.filePath, 'utf8') .then(function (xmlContent) { - return checkEmpty(xmlContent); + return checkEmpty(xmlContent) }) .then(_parseWithXml2js) .then( function (result) { - callback(null, result); + callback(null, result) }, function (e) { - callback(e, null); - }, - ); + callback(e, null) + } + ) } else if (opt.xmlContent) { // parse the xml provided by the api client. _parseWithXml2js(checkEmpty(opt.xmlContent)) .then(function (result) { - delete result.xmlContent; - callback(null, result); + delete result.xmlContent + callback(null, result) }) .catch(function (e) { - callback(e); - }); + callback(e) + }) } } @@ -165,19 +165,19 @@ function _parseWithXml2js(xmlContent: string): Promise { xml2js.parseString(xmlContent, XML2JS_OPTS, function (err, pomObject) { if (err) { // Reject with the error - reject(err); + reject(err) } // Replace the arrays with single elements with strings - removeSingleArrays(pomObject); + removeSingleArrays(pomObject) // Response to the call resolve({ pomXml: xmlContent, // Only add the pomXml when loaded from the file-system. - pomObject: pomObject, // Always add the object - }); - }); - }); + pomObject: pomObject // Always add the object + }) + }) + }) } /** @@ -189,21 +189,21 @@ function removeSingleArrays(obj: Object): void { traverse(obj).forEach(function traversing(value) { // As the XML parser returns single fields as arrays. if (value instanceof Array && value.length === 1) { - this.update(value[0]); + this.update(value[0]) } - }); + }) } function readFileAsync(path: string, encoding: BufferEncoding | undefined): Promise { return new Promise((resolve, reject) => fs.readFile(path, { encoding }, (err, data) => { if (err) { - reject(err); + reject(err) } else { - data instanceof Buffer ? resolve(data.toString(encoding)) : resolve(data); + data instanceof Buffer ? resolve(data.toString(encoding)) : resolve(data) } - }), - ); + }) + ) } -export default parse; +export default parse diff --git a/packages/maven/maven-schema.ts b/packages/maven/maven-schema.ts index 95a2bcc4..78a0e360 100644 --- a/packages/maven/maven-schema.ts +++ b/packages/maven/maven-schema.ts @@ -11,8 +11,8 @@ * License for the specific language governing permissions and limitations under the License. */ -import { object, array, string, InferType, ObjectSchema } from "yup"; -import { POM_MODEL_VERSION } from "./maven-constants"; +import { object, array, string, InferType, ObjectSchema } from 'yup' +import { POM_MODEL_VERSION } from './maven-constants' import { MavenProjectLicense, @@ -21,71 +21,71 @@ import { MavenProjectDependency, MavenProjectSourceControl, MavenProject as MavenProjectType, - MavenProjectManagedDependency, -} from "./maven-model"; + MavenProjectManagedDependency +} from './maven-model' -export type { MavenProjectLicense, MavenProjectContact, MavenProjectSourceControl, MavenProjectType }; +export type { MavenProjectLicense, MavenProjectContact, MavenProjectSourceControl, MavenProjectType } // Schema for a Maven project license. const mavenLicenseSchema: ObjectSchema = object({ - name: string().label("Name").required(), - url: string().label("URL").optional(), - distribution: string().label("Distribution").optional(), // add enum (@TODO) -}); + name: string().label('Name').required(), + url: string().label('URL').optional(), + distribution: string().label('Distribution').optional() // add enum (@TODO) +}) // Schema for a Maven developer or organization. const mavenContactSchema: ObjectSchema = object({ - id: string().label("ID").required(), - name: string().label("Name").optional(), - email: string().label("Email").optional(), - url: string().label("URL").optional(), - organization: string().label("Organization").optional(), - organizationUrl: string().label("Organization URL").optional(), -}); + id: string().label('ID').required(), + name: string().label('Name').optional(), + email: string().label('Email').optional(), + url: string().label('URL').optional(), + organization: string().label('Organization').optional(), + organizationUrl: string().label('Organization URL').optional() +}) // Shape for SCM settings. const scmType = { - url: string().label("URL").required(), - connection: string().label("Connection").required(), - developerConnection: string().label("Developer Connection").optional(), -}; + url: string().label('URL').required(), + connection: string().label('Connection').required(), + developerConnection: string().label('Developer Connection').optional() +} // Schema for a Maven project's source control. -export const mavenScmSchema: ObjectSchema = object(scmType); +export const mavenScmSchema: ObjectSchema = object(scmType) // Base for a Maven coordinate. const coordinateBase = { - groupId: string().label("Group").required(), - artifactId: string().label("Artifact").required(), - classifier: string().label("Classifier").optional(), -}; + groupId: string().label('Group').required(), + artifactId: string().label('Artifact').required(), + classifier: string().label('Classifier').optional() +} /** * Schema for a Maven coordinate. */ -export const coordinateSchema: ObjectSchema> = object(coordinateBase); +export const coordinateSchema: ObjectSchema> = object(coordinateBase) // Version-optional coordinate. const versionedOptionalCoordinateBase = { ...coordinateBase, - version: string().label("Version").optional(), -}; + version: string().label('Version').optional() +} // Version-required coordinate. const versionedRequiredCoordinateBase = { ...coordinateBase, - version: string().label("Version").required(), -}; + version: string().label('Version').required() +} // Exclusions property. const dependencyExclusions = { - exclusions: array(coordinateSchema).label("Exclusions").optional(), -}; + exclusions: array(coordinateSchema).label('Exclusions').optional() +} // Scope property. const dependencyScope = { - scope: string().label("Scope").optional(), // add enum (@TODO) -}; + scope: string().label('Scope').optional() // add enum (@TODO) +} /** * Schema for a dependency specified in a Maven project. @@ -93,32 +93,32 @@ const dependencyScope = { export const dependencySchema: ObjectSchema = object({ ...versionedOptionalCoordinateBase, ...dependencyExclusions, - ...dependencyScope, -}); + ...dependencyScope +}) /** * Schema for a managed dependency specified in a Maven project. */ export const managedDependencySchema: ObjectSchema = object({ ...versionedRequiredCoordinateBase, - ...dependencyExclusions, -}); + ...dependencyExclusions +}) /** * Schema for a full Maven project. */ export const mavenProjectSchema: ObjectSchema = object({ - modelVersion: string().label("Model Version").matches(new RegExp(POM_MODEL_VERSION)).required(), - name: string().label("Name").optional(), - description: string().label("Description").optional(), - url: string().label("URL").optional(), - packaging: string().label("Packaging").required(), // add enum (@TODO) - licenses: array(mavenLicenseSchema).label("Licenses").optional(), - developers: array(mavenContactSchema).label("Developers").optional(), - scm: object(scmType).label("SCM").optional(), - dependencyManagement: array(managedDependencySchema).label("Managed Dependencies").optional(), - dependencies: array(dependencySchema).label("Dependencies").optional(), -}); + modelVersion: string().label('Model Version').matches(new RegExp(POM_MODEL_VERSION)).required(), + name: string().label('Name').optional(), + description: string().label('Description').optional(), + url: string().label('URL').optional(), + packaging: string().label('Packaging').required(), // add enum (@TODO) + licenses: array(mavenLicenseSchema).label('Licenses').optional(), + developers: array(mavenContactSchema).label('Developers').optional(), + scm: object(scmType).label('SCM').optional(), + dependencyManagement: array(managedDependencySchema).label('Managed Dependencies').optional(), + dependencies: array(dependencySchema).label('Dependencies').optional() +}) /** * Maven Project: Schema diff --git a/packages/maven/package.json b/packages/maven/package.json index 9058a626..e2cf4da7 100644 --- a/packages/maven/package.json +++ b/packages/maven/package.json @@ -1,45 +1,27 @@ { "name": "@javamodules/maven", - "version": "1.0.1", - "type": "module", - "main": "dist/index.mjs", + "version": "1.0.2", "description": "Tools for working with Maven projects and metadata from JavaScript.", - "homepage": "https://github.com/javamodules", - "license": "Apache-2.0", - "files": [ - "dist/**", - "!dist/*test*", - "!dist/tests" - ], "keywords": [ "tools", "java", "maven" ], - "publishConfig": { - "provenance": true, - "access": "public" + "homepage": "https://github.com/javamodules", + "bugs": { + "url": "https://github.com/elide-dev/jpms/issues" }, "repository": { "type": "git", "url": "https://github.com/elide-dev/jpms", "directory": "packages/maven" }, - "bugs": { - "url": "https://github.com/elide-dev/jpms/issues" - }, + "license": "Apache-2.0", "author": { "name": "Sam Gammon", "url": "https://github.com/sgammon" }, - "scripts": { - "test:bun": "bun test", - "test:node": "node --experimental-vm-modules node_modules/jest/bin/jest.js", - "publish:dry": "npm publish --no-git-checks --dry-run", - "publish:live": "npm publish --no-git-checks", - "pack": "npm pack", - "build": "tsc -p ." - }, + "type": "module", "imports": { "#tests": { "bun": "bun:test", @@ -56,32 +38,29 @@ "types": "./dist/index.d.ts" } }, - "dependencies": { - "traverse": "0.6.8", - "xml2js": "0.6.2", - "yup": "1.4.0" - }, - "devDependencies": { - "@jest/globals": "29.7.0", - "@types/jest": "29.5.12", - "@types/node": "20.11.29", - "@types/traverse": "0.6.36", - "@types/xml2js": "0.4.14", - "jest": "29.7.0", - "jest-junit": "16.0.0", - "semver": "7.6.0", - "ts-jest": "29.1.2", - "typescript": "5.4.2" + "main": "dist/index.mjs", + "files": [ + "dist/**", + "!dist/*test*", + "!dist/tests" + ], + "scripts": { + "build": "tsc -p .", + "pack": "npm pack", + "publish:dry": "npm publish --no-git-checks --dry-run", + "publish:live": "npm publish --no-git-checks", + "test:bun": "bun test", + "test:node": "node --experimental-vm-modules node_modules/jest/bin/jest.js" }, "jest": { - "preset": "ts-jest", "collectCoverage": true, - "coverageProvider": "v8", "coverageDirectory": "reports", + "coverageProvider": "v8", "coverageReporters": [ "lcov", "text-summary" ], + "preset": "ts-jest", "reporters": [ "default", "github-actions", @@ -96,5 +75,26 @@ "testMatch": [ "/tests/*.test.ts" ] + }, + "dependencies": { + "traverse": "0.6.8", + "xml2js": "0.6.2", + "yup": "1.4.0" + }, + "devDependencies": { + "@jest/globals": "29.7.0", + "@types/jest": "29.5.12", + "@types/node": "20.11.29", + "@types/traverse": "0.6.36", + "@types/xml2js": "0.4.14", + "jest": "29.7.0", + "jest-junit": "16.0.0", + "semver": "7.6.0", + "ts-jest": "29.1.2", + "typescript": "5.4.2" + }, + "publishConfig": { + "access": "public", + "provenance": true } } diff --git a/packages/maven/tests/maven-coordinate.test.ts b/packages/maven/tests/maven-coordinate.test.ts index a4ad940d..a20827ad 100644 --- a/packages/maven/tests/maven-coordinate.test.ts +++ b/packages/maven/tests/maven-coordinate.test.ts @@ -11,46 +11,46 @@ * License for the specific language governing permissions and limitations under the License. */ -import { expect, test } from "@jest/globals"; -import { MavenCoordinate, mavenCoordinate, parseMavenCoordinate } from "../maven-model"; +import { expect, test } from '@jest/globals' +import { MavenCoordinate, mavenCoordinate, parseMavenCoordinate } from '../maven-model' -test("build a maven coordinate", () => { - const coordinate: MavenCoordinate = mavenCoordinate("com.google.guava", "guava", "1.0.0"); - expect(coordinate).not.toBeNull(); - expect(coordinate.groupId).toBe("com.google.guava"); - expect(coordinate.artifactId).toBe("guava"); - expect(coordinate.version).toBe("1.0.0"); -}); +test('build a maven coordinate', () => { + const coordinate: MavenCoordinate = mavenCoordinate('com.google.guava', 'guava', '1.0.0') + expect(coordinate).not.toBeNull() + expect(coordinate.groupId).toBe('com.google.guava') + expect(coordinate.artifactId).toBe('guava') + expect(coordinate.version).toBe('1.0.0') +}) -test("build a maven coordinate with a classifier", () => { - const coordinate: MavenCoordinate = mavenCoordinate("com.google.guava", "guava", "1.0.0", "linux-amd64"); - expect(coordinate).not.toBeNull(); - expect(coordinate.groupId).toBe("com.google.guava"); - expect(coordinate.artifactId).toBe("guava"); - expect(coordinate.version).toBe("1.0.0"); - expect(coordinate.classifier).toBe("linux-amd64"); -}); +test('build a maven coordinate with a classifier', () => { + const coordinate: MavenCoordinate = mavenCoordinate('com.google.guava', 'guava', '1.0.0', 'linux-amd64') + expect(coordinate).not.toBeNull() + expect(coordinate.groupId).toBe('com.google.guava') + expect(coordinate.artifactId).toBe('guava') + expect(coordinate.version).toBe('1.0.0') + expect(coordinate.classifier).toBe('linux-amd64') +}) -test("build a maven coordinate to string", () => { - const coordinate: MavenCoordinate = mavenCoordinate("com.google.guava", "guava", "1.0.0"); - expect(coordinate).not.toBeNull(); - expect(coordinate.groupId).toBe("com.google.guava"); - expect(coordinate.artifactId).toBe("guava"); - expect(coordinate.version).toBe("1.0.0"); - expect(coordinate.valueOf()).toBe("com.google.guava:guava:1.0.0"); -}); +test('build a maven coordinate to string', () => { + const coordinate: MavenCoordinate = mavenCoordinate('com.google.guava', 'guava', '1.0.0') + expect(coordinate).not.toBeNull() + expect(coordinate.groupId).toBe('com.google.guava') + expect(coordinate.artifactId).toBe('guava') + expect(coordinate.version).toBe('1.0.0') + expect(coordinate.valueOf()).toBe('com.google.guava:guava:1.0.0') +}) -test("parsing a maven coordinate from a string", () => { - const coordinate: MavenCoordinate = mavenCoordinate("com.google.guava", "guava", "1.0.0"); - expect(coordinate).not.toBeNull(); - expect(coordinate.groupId).toBe("com.google.guava"); - expect(coordinate.artifactId).toBe("guava"); - expect(coordinate.version).toBe("1.0.0"); - expect(coordinate.valueOf()).toBe("com.google.guava:guava:1.0.0"); - const parsed: MavenCoordinate = parseMavenCoordinate(coordinate.valueOf() as string); - expect(parsed).not.toBeNull(); - expect(parsed.groupId).toBe("com.google.guava"); - expect(parsed.artifactId).toBe("guava"); - expect(parsed.version).toBe("1.0.0"); - expect(parsed.valueOf()).toBe(coordinate.valueOf()); -}); +test('parsing a maven coordinate from a string', () => { + const coordinate: MavenCoordinate = mavenCoordinate('com.google.guava', 'guava', '1.0.0') + expect(coordinate).not.toBeNull() + expect(coordinate.groupId).toBe('com.google.guava') + expect(coordinate.artifactId).toBe('guava') + expect(coordinate.version).toBe('1.0.0') + expect(coordinate.valueOf()).toBe('com.google.guava:guava:1.0.0') + const parsed: MavenCoordinate = parseMavenCoordinate(coordinate.valueOf() as string) + expect(parsed).not.toBeNull() + expect(parsed.groupId).toBe('com.google.guava') + expect(parsed.artifactId).toBe('guava') + expect(parsed.version).toBe('1.0.0') + expect(parsed.valueOf()).toBe(coordinate.valueOf()) +}) diff --git a/packages/maven/tests/maven-pom-parse.test.ts b/packages/maven/tests/maven-pom-parse.test.ts index 2075c5ab..d83e6df1 100644 --- a/packages/maven/tests/maven-pom-parse.test.ts +++ b/packages/maven/tests/maven-pom-parse.test.ts @@ -11,256 +11,256 @@ * License for the specific language governing permissions and limitations under the License. */ -import { expect, test } from "@jest/globals"; -import { join, resolve } from "node:path"; -import { existsSync } from "node:fs"; -import { POM_CONTENT_PARENT } from "./maven-samples"; -import { MavenProjectPackaging } from "../maven-model"; -import parser, { parseAsync, ParsedOutput } from "../maven-parser"; +import { expect, test } from '@jest/globals' +import { join, resolve } from 'node:path' +import { existsSync } from 'node:fs' +import { POM_CONTENT_PARENT } from './maven-samples' +import { MavenProjectPackaging } from '../maven-model' +import parser, { parseAsync, ParsedOutput } from '../maven-parser' -test("parse basic pom content (callback)", async () => { +test('parse basic pom content (callback)', async () => { // sanity: should have non-null string to parse - expect(POM_CONTENT_PARENT).not.toBeNull(); + expect(POM_CONTENT_PARENT).not.toBeNull() // parse it const parsed: Promise = new Promise((accept, reject) => { parser({ xmlContent: POM_CONTENT_PARENT }, (err: Error | null, result: ParsedOutput | null | undefined) => { // callback with parsed value - if (err !== null) reject(err); - else accept(result as ParsedOutput); - }); - }); - const result: ParsedOutput = await parsed; + if (err !== null) reject(err) + else accept(result as ParsedOutput) + }) + }) + const result: ParsedOutput = await parsed - expect(result).not.toBeNull(); - expect(result.pomXml).toBeDefined(); - expect(result.pomObject).toBeDefined(); - expect(result.pomObject.project).toBeDefined(); - const project = result.pomObject.project; - expect(project.xmlns).toBe("http://maven.apache.org/POM/4.0.0"); - expect(project["xmlns:xsi"]).toBe("http://www.w3.org/2001/XMLSchema-instance"); - expect(project["xsi:schemaLocation"]).toBe( - "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd", - ); - expect(project.parent).toBeDefined(); - const parent = project.parent; + expect(result).not.toBeNull() + expect(result.pomXml).toBeDefined() + expect(result.pomObject).toBeDefined() + expect(result.pomObject.project).toBeDefined() + const project = result.pomObject.project + expect(project.xmlns).toBe('http://maven.apache.org/POM/4.0.0') + expect(project['xmlns:xsi']).toBe('http://www.w3.org/2001/XMLSchema-instance') + expect(project['xsi:schemaLocation']).toBe( + 'http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd' + ) + expect(project.parent).toBeDefined() + const parent = project.parent const projectAssertions = (thing: any) => { - expect(thing.artifactid).toBeDefined(); - expect(thing.groupid).toBeDefined(); - expect(thing.version).toBeDefined(); - expect(thing.artifactid).not.toBe(""); - expect(thing.groupid).not.toBe(""); - expect(thing.version).not.toBe(""); - }; - projectAssertions(project); - projectAssertions(parent); + expect(thing.artifactid).toBeDefined() + expect(thing.groupid).toBeDefined() + expect(thing.version).toBeDefined() + expect(thing.artifactid).not.toBe('') + expect(thing.groupid).not.toBe('') + expect(thing.version).not.toBe('') + } + projectAssertions(project) + projectAssertions(parent) - expect(project.modelversion._).toBe("4.0.0"); - expect(project.packaging).toBe(MavenProjectPackaging.POM); - expect(project.name).toBe("Some Example Library"); -}); + expect(project.modelversion._).toBe('4.0.0') + expect(project.packaging).toBe(MavenProjectPackaging.POM) + expect(project.name).toBe('Some Example Library') +}) -test("parse basic pom content (async)", async () => { +test('parse basic pom content (async)', async () => { // sanity: should have non-null string to parse - expect(POM_CONTENT_PARENT).not.toBeNull(); + expect(POM_CONTENT_PARENT).not.toBeNull() // parse it - const result = await parseAsync({ xmlContent: POM_CONTENT_PARENT }); - expect(result).not.toBeNull(); - expect(result.pomXml).toBeDefined(); - expect(result.pomObject).toBeDefined(); - expect(result.pomObject.project).toBeDefined(); - const project = result.pomObject.project; - expect(project.xmlns).toBe("http://maven.apache.org/POM/4.0.0"); - expect(project["xmlns:xsi"]).toBe("http://www.w3.org/2001/XMLSchema-instance"); - expect(project["xsi:schemaLocation"]).toBe( - "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd", - ); - expect(project.parent).toBeDefined(); - const parent = project.parent; + const result = await parseAsync({ xmlContent: POM_CONTENT_PARENT }) + expect(result).not.toBeNull() + expect(result.pomXml).toBeDefined() + expect(result.pomObject).toBeDefined() + expect(result.pomObject.project).toBeDefined() + const project = result.pomObject.project + expect(project.xmlns).toBe('http://maven.apache.org/POM/4.0.0') + expect(project['xmlns:xsi']).toBe('http://www.w3.org/2001/XMLSchema-instance') + expect(project['xsi:schemaLocation']).toBe( + 'http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd' + ) + expect(project.parent).toBeDefined() + const parent = project.parent const projectAssertions = (thing: any) => { - expect(thing.artifactid).toBeDefined(); - expect(thing.groupid).toBeDefined(); - expect(thing.version).toBeDefined(); - expect(thing.artifactid).not.toBe(""); - expect(thing.groupid).not.toBe(""); - expect(thing.version).not.toBe(""); - }; - projectAssertions(project); - projectAssertions(parent); + expect(thing.artifactid).toBeDefined() + expect(thing.groupid).toBeDefined() + expect(thing.version).toBeDefined() + expect(thing.artifactid).not.toBe('') + expect(thing.groupid).not.toBe('') + expect(thing.version).not.toBe('') + } + projectAssertions(project) + projectAssertions(parent) - expect(project.modelversion._).toBe("4.0.0"); - expect(project.packaging).toBe(MavenProjectPackaging.POM); - expect(project.name).toBe("Some Example Library"); -}); + expect(project.modelversion._).toBe('4.0.0') + expect(project.packaging).toBe(MavenProjectPackaging.POM) + expect(project.name).toBe('Some Example Library') +}) test("parse guava's pom file (callback)", async () => { - const path = resolve(join(__dirname, "guava.xml")); - expect(existsSync(path)).toBeTruthy(); + const path = resolve(join(__dirname, 'guava.xml')) + expect(existsSync(path)).toBeTruthy() const parsed = new Promise((accept, reject) => { parser({ filePath: path }, (err: Error | null, result: ParsedOutput | null | undefined) => { // callback with parsed value - if (err !== null) reject(err); - else accept(result); - }); - }); - const result = await parsed; - expect(result).toBeDefined(); -}); + if (err !== null) reject(err) + else accept(result) + }) + }) + const result = await parsed + expect(result).toBeDefined() +}) test("parse guava's pom file (async)", async () => { - const path = resolve(join(__dirname, "guava.xml")); - expect(existsSync(path)).toBeTruthy(); - const result = await parseAsync({ filePath: path }); - expect(result).toBeDefined(); -}); + const path = resolve(join(__dirname, 'guava.xml')) + expect(existsSync(path)).toBeTruthy() + const result = await parseAsync({ filePath: path }) + expect(result).toBeDefined() +}) -test("must provide content or a file to parse (callback)", () => { - let caught = false; +test('must provide content or a file to parse (callback)', () => { + let caught = false try { - parser({}, () => {}); + parser({}, () => {}) } catch (err) { - caught = true; + caught = true } - expect(caught).toBeTruthy(); -}); + expect(caught).toBeTruthy() +}) -test("must provide content or a file to parse (async)", async () => { - let caught = false; +test('must provide content or a file to parse (async)', async () => { + let caught = false try { - await parseAsync({}); + await parseAsync({}) } catch (err) { - caught = true; + caught = true } - expect(caught).toBeTruthy(); -}); + expect(caught).toBeTruthy() +}) -test("must provide options to parser (callback)", () => { - let caught = false; +test('must provide options to parser (callback)', () => { + let caught = false try { - parser(null, () => {}); + parser(null, () => {}) } catch (err) { - caught = true; + caught = true } - expect(caught).toBeTruthy(); -}); + expect(caught).toBeTruthy() +}) -test("must provide options to parser (async)", async () => { - let caught = false; +test('must provide options to parser (async)', async () => { + let caught = false try { - await parseAsync(null); + await parseAsync(null) } catch (err) { - caught = true; + caught = true } - expect(caught).toBeTruthy(); -}); + expect(caught).toBeTruthy() +}) -test("requested pom file must exist (callback)", async () => { - let caught = false; +test('requested pom file must exist (callback)', async () => { + let caught = false try { - await parseAsync({ filePath: "i/do/not/exist.pom" }); + await parseAsync({ filePath: 'i/do/not/exist.pom' }) } catch (err) { - caught = true; + caught = true } - expect(caught).toBeTruthy(); -}); + expect(caught).toBeTruthy() +}) -test("requested pom file must exist (async)", async () => { - let caught = false; +test('requested pom file must exist (async)', async () => { + let caught = false try { await new Promise((accept, reject) => { - parser({ filePath: "i/do/not/exist.pom" }, (err: Error | null, result: ParsedOutput | null | undefined) => { + parser({ filePath: 'i/do/not/exist.pom' }, (err: Error | null, result: ParsedOutput | null | undefined) => { // callback with parsed value - if (err !== null) reject(err); - else accept(result); - }); - }); + if (err !== null) reject(err) + else accept(result) + }) + }) } catch (err) { - caught = true; + caught = true } - expect(caught).toBeTruthy(); -}); + expect(caught).toBeTruthy() +}) -test("requested pom file must not be empty (callback)", async () => { - const path = resolve(join(__dirname, "empty.xml")); - let caught = false; +test('requested pom file must not be empty (callback)', async () => { + const path = resolve(join(__dirname, 'empty.xml')) + let caught = false try { - await parseAsync({ filePath: path }); + await parseAsync({ filePath: path }) } catch (err) { - caught = true; + caught = true } - expect(caught).toBeTruthy(); -}); + expect(caught).toBeTruthy() +}) -test("requested pom file must not be empty (async)", async () => { - const path = resolve(join(__dirname, "empty.xml")); - let caught = false; +test('requested pom file must not be empty (async)', async () => { + const path = resolve(join(__dirname, 'empty.xml')) + let caught = false try { await new Promise((accept, reject) => { parser({ filePath: path }, (err: Error | null, result: ParsedOutput | null | undefined) => { // callback with parsed value - if (err !== null) reject(err); - else accept(result); - }); - }); + if (err !== null) reject(err) + else accept(result) + }) + }) } catch (err) { - caught = true; + caught = true } - expect(caught).toBeTruthy(); -}); + expect(caught).toBeTruthy() +}) -test("requested pom file must be valid xml (callback)", async () => { - const path = resolve(join(__dirname, "not-valid-xml.xml")); - let caught = false; +test('requested pom file must be valid xml (callback)', async () => { + const path = resolve(join(__dirname, 'not-valid-xml.xml')) + let caught = false try { - await parseAsync({ filePath: path }); + await parseAsync({ filePath: path }) } catch (err) { - caught = true; + caught = true } - expect(caught).toBeTruthy(); -}); + expect(caught).toBeTruthy() +}) -test("requested pom file must be valid xml (async)", async () => { - const path = resolve(join(__dirname, "not-valid-xml.xml")); - let caught = false; +test('requested pom file must be valid xml (async)', async () => { + const path = resolve(join(__dirname, 'not-valid-xml.xml')) + let caught = false try { await new Promise((accept, reject) => { parser({ filePath: path }, (err: Error | null, result: ParsedOutput | null | undefined) => { // callback with parsed value - if (err !== null) reject(err); - else accept(result); - }); - }); + if (err !== null) reject(err) + else accept(result) + }) + }) } catch (err) { - caught = true; + caught = true } - expect(caught).toBeTruthy(); -}); + expect(caught).toBeTruthy() +}) -test("requested pom content must be valid xml (callback)", async () => { - let caught = false; +test('requested pom content must be valid xml (callback)', async () => { + let caught = false try { - await parseAsync({ xmlContent: "{}" }); + await parseAsync({ xmlContent: '{}' }) } catch (err) { - caught = true; + caught = true } - expect(caught).toBeTruthy(); -}); + expect(caught).toBeTruthy() +}) -test("requested pom content must be valid xml (async)", async () => { - let caught = false; +test('requested pom content must be valid xml (async)', async () => { + let caught = false try { await new Promise((accept, reject) => { - parser({ xmlContent: "{}" }, (err: Error | null, result: ParsedOutput | null | undefined) => { + parser({ xmlContent: '{}' }, (err: Error | null, result: ParsedOutput | null | undefined) => { // callback with parsed value - if (err !== null) reject(err); - else accept(result); - }); - }); + if (err !== null) reject(err) + else accept(result) + }) + }) } catch (err) { - caught = true; + caught = true } - expect(caught).toBeTruthy(); -}); + expect(caught).toBeTruthy() +}) diff --git a/packages/maven/tests/maven-samples.ts b/packages/maven/tests/maven-samples.ts index e6024de2..e7c435f7 100644 --- a/packages/maven/tests/maven-samples.ts +++ b/packages/maven/tests/maven-samples.ts @@ -14,40 +14,40 @@ export const POM_CONTENT_PARENT = '' + - " " + - " example-parent" + - " org.example" + - " 0.0.1" + - " " + + ' ' + + ' example-parent' + + ' org.example' + + ' 0.0.1' + + ' ' + ' 4.0.0' + - " org.example" + - " example" + - " 0.0.1-SNAPSHOT" + - " pom" + - " Some Example Library" + - ""; + ' org.example' + + ' example' + + ' 0.0.1-SNAPSHOT' + + ' pom' + + ' Some Example Library' + + '' export const POM_CONTENT_NO_PARENT = '' + ' 4.0.0' + - " org.example" + - " example" + - " 0.0.1-SNAPSHOT" + - " pom" + - " Some Example Library" + - ""; + ' org.example' + + ' example' + + ' 0.0.1-SNAPSHOT' + + ' pom' + + ' Some Example Library' + + '' export const POM_CONTENT_PARENT_INHERITED = '' + - " " + - " example-parent" + - " org.example" + - " 0.0.1" + - " " + + ' ' + + ' example-parent' + + ' org.example' + + ' 0.0.1' + + ' ' + ' 4.0.0' + - " example" + - " pom" + - " Some Example Library" + - ""; + ' example' + + ' pom' + + ' Some Example Library' + + '' diff --git a/pages/0-guide.md b/pages/0-guide.md index d4a826d9..b97a45ee 100644 --- a/pages/0-guide.md +++ b/pages/0-guide.md @@ -1,22 +1,32 @@ # JPMS Attic Repository -This usage guide can help you use the [Java Platform Module System][0] in your build before popular libraries ship support for it. Libraries like **Guava**, **Reactive Streams**, and **Protobuf** don't yet ship `module-info` definitions; some specify an `Automatic-Module-Name`, but tools like [`jlink`][1] reject these. +This usage guide can help you use the [Java Platform Module System][0] in your build before popular libraries ship +support for it. Libraries like **Guava**, **Reactive Streams**, and **Protobuf** don't yet ship `module-info` +definitions; some specify an `Automatic-Module-Name`, but tools like [`jlink`][1] reject these. ## So you want to use Modular Java If you want to use these libraries in a module app, you are left with a poor set of choices: -**1) 🙅 Build your app with these libraries on the classpath.** -You won't be able to use tools like `jlink`, but perhaps you give those up; well, libraries on the `classpath` are not subject to encapsulation rules even for modular Java builds. This largely _defeats the purpose_ of a modular Java build, because all classes can be seen by these artifacts, so no safe optimization can be achieved by the compiler. +**1) 🙅 Build your app with these libraries on the classpath.** You won't be able to use tools like `jlink`, but perhaps +you give those up; well, libraries on the `classpath` are not subject to encapsulation rules even for modular Java +builds. This largely _defeats the purpose_ of a modular Java build, because all classes can be seen by these artifacts, +so no safe optimization can be achieved by the compiler. -**2) 🙅 Use tools like [Moditect][2].** -Moditect is pretty great for injecting or generating `module-info.java` definitions when you use these libraries at dependencies. The downside, of course, is that you have to modify your dependencies, and wire together Moditect in your build. For some this is hard, for others this is intolerable, and for another group it is perfect. +**2) 🙅 Use tools like [Moditect][2].** Moditect is pretty great for injecting or generating `module-info.java` +definitions when you use these libraries at dependencies. The downside, of course, is that you have to modify your +dependencies, and wire together Moditect in your build. For some this is hard, for others this is intolerable, and for +another group it is perfect. -**3) 🙅 Stop using the libraries.** -You could stop using libraries like Guava, Reactive Streams, and Protobuf, but these libraries are used _so ubiquitously_ in the Java ecosystem that you are likely to run into them in your transitive library graph anyway. If you want to use Google Cloud someday, you will _have_ to use all of these. If you want to use popular libraries like Dubbo or FoundationDB that are downstream from Guava and Protobuf, they will end up in your graph nonetheless, and you can return to step 1 or 2. +**3) 🙅 Stop using the libraries.** You could stop using libraries like Guava, Reactive Streams, and Protobuf, but these +libraries are used _so ubiquitously_ in the Java ecosystem that you are likely to run into them in your transitive +library graph anyway. If you want to use Google Cloud someday, you will _have_ to use all of these. If you want to use +popular libraries like Dubbo or FoundationDB that are downstream from Guava and Protobuf, they will end up in your graph +nonetheless, and you can return to step 1 or 2. -**4) 🙅 Stop using JPMS.** -This is an option, and even the [best of us][3] have resorted to this at times. But JPMS **is not going away**, offers meaningful compile time and safety optimizations, unlocks new tools like `jlink`, and is even required for [certain advanced use cases of Java][4]. +**4) 🙅 Stop using JPMS.** This is an option, and even the [best of us][3] have resorted to this at times. But JPMS **is +not going away**, offers meaningful compile time and safety optimizations, unlocks new tools like `jlink`, and is even +required for [certain advanced use cases of Java][4]. --- @@ -30,7 +40,8 @@ This [GitHub repository][5] (the one hosting this page) is also a Maven Reposito https://jpms.pkg.st/repository ``` -This repository forks the popular libraries you need, with fully modular Java support, until such time as modular support is released to Maven Central. +This repository forks the popular libraries you need, with fully modular Java support, until such time as modular +support is released to Maven Central. ## How you can use it @@ -39,7 +50,8 @@ This repository forks the popular libraries you need, with fully modular Java su - As a [Gradle Version Catalog][7] - As a [Gradle Platform][8] -Together these artifacts let you consume the libraries in this repo easily (and safely, in a way that does not collide with Maven Central), and also **enforce transitively** that your artifact graph will support JPMS. +Together these artifacts let you consume the libraries in this repo easily (and safely, in a way that does not collide +with Maven Central), and also **enforce transitively** that your artifact graph will support JPMS. [0]: https://www.oracle.com/corporate/features/understanding-java-9-modules.html [1]: https://docs.oracle.com/en/java/javase/11/tools/jlink.html diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb04a22d..728fa9b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: prettier: specifier: 3.2.5 version: 3.2.5 + prettier-plugin-packagejson: + specifier: 2.4.12 + version: 2.4.12(prettier@3.2.5) typescript: specifier: 5.4.2 version: 5.4.2 @@ -66,9 +69,9 @@ importers: packages/indexer: dependencies: - '@endo/zip': - specifier: 1.0.2 - version: 1.0.2 + '@cloudpss/zstd': + specifier: 0.2.15 + version: 0.2.15 '@javamodules/gradle': specifier: workspace:* version: link:../gradle @@ -78,18 +81,30 @@ importers: '@javamodules/maven': specifier: workspace:* version: link:../maven + '@sqlite.org/sqlite-wasm': + specifier: 3.45.2-build1 + version: 3.45.2-build1 chalk: specifier: 5.3.0 version: 5.3.0 commander: specifier: 12.0.0 version: 12.0.0 + fflate: + specifier: 0.8.2 + version: 0.8.2 glob: specifier: 10.3.10 version: 10.3.10 inquirer: specifier: 9.2.16 version: 9.2.16 + snappy-wasm: + specifier: 0.3.0 + version: 0.3.0 + wasm-gzip: + specifier: 2.0.3 + version: 2.0.3 devDependencies: '@jest/globals': specifier: 29.7.0 @@ -118,15 +133,18 @@ importers: packages/java: dependencies: - '@endo/zip': - specifier: 1.0.2 - version: 1.0.2 bytebuffer: specifier: 5.0.1 version: 5.0.1 + fflate: + specifier: 0.8.2 + version: 0.8.2 glob: specifier: 10.3.10 version: 10.3.10 + memfs: + specifier: 4.8.0 + version: 4.8.0 semver: specifier: 7.6.0 version: 7.6.0 @@ -729,6 +747,15 @@ packages: prettier: 2.8.8 dev: true + /@cloudpss/zstd@0.2.15: + resolution: {integrity: sha512-6Ippsasx/IU9dLs6eTscpZP5tUkSKgqCAPgqlFaC5Gjg/a0+PhMsf4MRACZKiBJ+qJAImdyTmhItE7hBIP47OQ==, tarball: https://registry.npmjs.org/@cloudpss/zstd/-/zstd-0.2.15.tgz} + engines: {node: '>=14.16'} + requiresBuild: true + dependencies: + node-addon-api: 8.0.0 + node-gyp-build: 4.8.0 + dev: false + /@commitlint/cli@19.2.0(@types/node@20.11.29)(typescript@5.4.2): resolution: {integrity: sha512-8XnQDMyQR+1/ldbmIyhonvnDS2enEw48Wompo/967fsEvy9Vj5/JbDutzmSBKxANWDVeEbR9QQm0yHpw6ArrFw==, tarball: https://registry.npmjs.org/@commitlint/cli/-/cli-19.2.0.tgz} engines: {node: '>=v18'} @@ -892,10 +919,6 @@ packages: chalk: 5.3.0 dev: true - /@endo/zip@1.0.2: - resolution: {integrity: sha512-+CuclRetpit92j68XVjW5vPLuloKkwKwAW38eGaQKEUIeGG28VJdypwSFRHn9uFBTLJM0iPaAmB/P0xt8ywX5w==, tarball: https://registry.npmjs.org/@endo/zip/-/zip-1.0.2.tgz} - dev: false - /@isaacs/cliui@8.0.2: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==, tarball: https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz} engines: {node: '>=12'} @@ -1223,6 +1246,11 @@ packages: dev: false optional: true + /@pkgr/core@0.1.1: + resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==, tarball: https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + dev: true + /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==, tarball: https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz} dev: true @@ -1239,6 +1267,11 @@ packages: '@sinonjs/commons': 3.0.1 dev: true + /@sqlite.org/sqlite-wasm@3.45.2-build1: + resolution: {integrity: sha512-TeQf8rNFQna8pg95IshtpXkhdMq1iNAXllCoNrhEn18660Hbal9yxKAYv+ZfC1kjfdEXMK6V0XanfX5OT5FyMw==, tarball: https://registry.npmjs.org/@sqlite.org/sqlite-wasm/-/sqlite-wasm-3.45.2-build1.tgz} + hasBin: true + dev: false + /@types/babel__core@7.20.5: resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==, tarball: https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz} dependencies: @@ -2053,11 +2086,21 @@ packages: engines: {node: '>=8'} dev: true + /detect-indent@7.0.1: + resolution: {integrity: sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==, tarball: https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.1.tgz} + engines: {node: '>=12.20'} + dev: true + /detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==, tarball: https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz} engines: {node: '>=8'} dev: true + /detect-newline@4.0.1: + resolution: {integrity: sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==, tarball: https://registry.npmjs.org/detect-newline/-/detect-newline-4.0.1.tgz} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + /diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==, tarball: https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2373,6 +2416,10 @@ packages: bser: 2.1.1 dev: true + /fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==, tarball: https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz} + dev: false + /figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==, tarball: https://registry.npmjs.org/figures/-/figures-3.2.0.tgz} engines: {node: '>=8'} @@ -2510,6 +2557,11 @@ packages: engines: {node: '>=8.0.0'} dev: true + /get-stdin@9.0.0: + resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==, tarball: https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz} + engines: {node: '>=12'} + dev: true + /get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==, tarball: https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz} engines: {node: '>=10'} @@ -2529,6 +2581,10 @@ packages: get-intrinsic: 1.2.4 dev: true + /git-hooks-list@3.1.0: + resolution: {integrity: sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA==, tarball: https://registry.npmjs.org/git-hooks-list/-/git-hooks-list-3.1.0.tgz} + dev: true + /git-raw-commits@4.0.0: resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==, tarball: https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-4.0.0.tgz} engines: {node: '>=16'} @@ -2600,6 +2656,17 @@ packages: slash: 3.0.0 dev: true + /globby@13.2.2: + resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==, tarball: https://registry.npmjs.org/globby/-/globby-13.2.2.tgz} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.1 + merge2: 1.4.1 + slash: 4.0.0 + dev: true + /gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==, tarball: https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz} dependencies: @@ -2892,6 +2959,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==, tarball: https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz} + engines: {node: '>=12'} + dev: true + /is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==, tarball: https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz} engines: {node: '>= 0.4'} @@ -3728,6 +3800,13 @@ packages: engines: {node: '>=8'} dev: true + /memfs@4.8.0: + resolution: {integrity: sha512-fcs7trFxZlOMadmTw5nyfOwS3il9pr3y+6xzLfXNwmuR/D0i4wz6rJURxArAbcJDGalbpbMvQ/IFI0NojRZgRg==, tarball: https://registry.npmjs.org/memfs/-/memfs-4.8.0.tgz} + engines: {node: '>= 4.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + /meow@12.1.1: resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==, tarball: https://registry.npmjs.org/meow/-/meow-12.1.1.tgz} engines: {node: '>=16.10'} @@ -3836,6 +3915,16 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, tarball: https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz} dev: true + /node-addon-api@8.0.0: + resolution: {integrity: sha512-ipO7rsHEBqa9STO5C5T10fj732ml+5kLN1cAG8/jdHd56ldQeGj3Q7+scUS+VHK/qy1zLEwC4wMK5+yM0btPvw==, tarball: https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.0.0.tgz} + engines: {node: ^18 || ^20 || >= 21} + dev: false + + /node-gyp-build@4.8.0: + resolution: {integrity: sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==, tarball: https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz} + hasBin: true + dev: false + /node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==, tarball: https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz} dev: true @@ -4097,6 +4186,19 @@ packages: which-pm: 2.0.0 dev: true + /prettier-plugin-packagejson@2.4.12(prettier@3.2.5): + resolution: {integrity: sha512-hifuuOgw5rHHTdouw9VrhT8+Nd7UwxtL1qco8dUfd4XUFQL6ia3xyjSxhPQTsGnSYFraTWy5Omb+MZm/OWDTpQ==, tarball: https://registry.npmjs.org/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.4.12.tgz} + peerDependencies: + prettier: '>= 1.16.0' + peerDependenciesMeta: + prettier: + optional: true + dependencies: + prettier: 3.2.5 + sort-package-json: 2.8.0 + synckit: 0.9.0 + dev: true + /prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==, tarball: https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz} engines: {node: '>=10.13.0'} @@ -4423,6 +4525,11 @@ packages: engines: {node: '>=8'} dev: true + /slash@4.0.0: + resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==, tarball: https://registry.npmjs.org/slash/-/slash-4.0.0.tgz} + engines: {node: '>=12'} + dev: true + /slice-ansi@5.0.0: resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==, tarball: https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz} engines: {node: '>=12'} @@ -4452,6 +4559,27 @@ packages: yargs: 15.4.1 dev: true + /snappy-wasm@0.3.0: + resolution: {integrity: sha512-mT2q7y2K0krf2WdsfR7YZL/vJ+BiTKcvgxIA/gJozPQibWJwV8VOl6TNZLjeUlGH2bqee/570fS/LUkO406Ixg==, tarball: https://registry.npmjs.org/snappy-wasm/-/snappy-wasm-0.3.0.tgz} + dev: false + + /sort-object-keys@1.1.3: + resolution: {integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==, tarball: https://registry.npmjs.org/sort-object-keys/-/sort-object-keys-1.1.3.tgz} + dev: true + + /sort-package-json@2.8.0: + resolution: {integrity: sha512-PxeNg93bTJWmDGnu0HADDucoxfFiKkIr73Kv85EBThlI1YQPdc0XovBgg2llD0iABZbu2SlKo8ntGmOP9wOj/g==, tarball: https://registry.npmjs.org/sort-package-json/-/sort-package-json-2.8.0.tgz} + hasBin: true + dependencies: + detect-indent: 7.0.1 + detect-newline: 4.0.1 + get-stdin: 9.0.0 + git-hooks-list: 3.1.0 + globby: 13.2.2 + is-plain-obj: 4.1.0 + sort-object-keys: 1.1.3 + dev: true + /source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==, tarball: https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz} dependencies: @@ -4655,6 +4783,14 @@ packages: engines: {node: '>= 0.4'} dev: true + /synckit@0.9.0: + resolution: {integrity: sha512-7RnqIMq572L8PeEzKeBINYEJDDxpcH8JEgLwUqBd3TkofhFRbkq4QLR0u+36avGAhCRbk2nnmjcW9SE531hPDg==, tarball: https://registry.npmjs.org/synckit/-/synckit-0.9.0.tgz} + engines: {node: ^14.18.0 || >=16.0.0} + dependencies: + '@pkgr/core': 0.1.1 + tslib: 2.6.2 + dev: true + /term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==, tarball: https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz} engines: {node: '>=8'} @@ -4754,7 +4890,6 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==, tarball: https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz} - dev: false /tty-table@4.2.3: resolution: {integrity: sha512-Fs15mu0vGzCrj8fmJNP7Ynxt5J7praPXqFN0leZeZBXJwkMxv9cb2D454k1ltrtUSJbZ4yH4e0CynsHLxmUfFA==, tarball: https://registry.npmjs.org/tty-table/-/tty-table-4.2.3.tgz} @@ -4930,6 +5065,10 @@ packages: makeerror: 1.0.12 dev: true + /wasm-gzip@2.0.3: + resolution: {integrity: sha512-HX0F/gSliIYLkNbpqH0uFjJWOcDFjn3mN1/FQx/j3XvHgig8asGZM5hP7lznWbHvma/iZaQtC8LAT05RaLB+qw==, tarball: https://registry.npmjs.org/wasm-gzip/-/wasm-gzip-2.0.3.tgz} + dev: false + /wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==, tarball: https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz} dependencies: diff --git a/samples/README.md b/samples/README.md index a024ce55..d2645dcb 100644 --- a/samples/README.md +++ b/samples/README.md @@ -1,11 +1,14 @@ # JPMS Attic: Samples -This sub-directory provides sample provides for Maven and Gradle which use the JPMS Attic repository. See each sample README for more information. +This sub-directory provides sample provides for Maven and Gradle which use the JPMS Attic repository. See each sample +README for more information. ## Available Samples - **[`modular-guava`](./modular-guava)**: Pure-Java project which builds against Guava JPMS with the local repository. -- **[`modular-guava-repo`](./modular-guava-repo)**: Same project, but builds with the remote repo, at `https://jpms.pkg.st/repository`. +- **[`modular-guava-repo`](./modular-guava-repo)**: Same project, but builds with the remote repo, at + `https://jpms.pkg.st/repository`. -- **[`modular-guava-maven`](./modular-guava-maven)**: Roughly the same project, but builds with the remote repo, at `https://jpms.pkg.st/repository`, with Maven. +- **[`modular-guava-maven`](./modular-guava-maven)**: Roughly the same project, but builds with the remote repo, at + `https://jpms.pkg.st/repository`, with Maven. diff --git a/samples/gradle-platform/README.md b/samples/gradle-platform/README.md index 910b2c6c..570fd3b2 100644 --- a/samples/gradle-platform/README.md +++ b/samples/gradle-platform/README.md @@ -1,3 +1,4 @@ ## Modular Guava -Sample pure-Java Gradle project which uses the libraries provided by this repository in order to build a fully modular Java app that makes use of Guava. +Sample pure-Java Gradle project which uses the libraries provided by this repository in order to build a fully modular +Java app that makes use of Guava. diff --git a/samples/modular-guava-maven/README.md b/samples/modular-guava-maven/README.md index 615edd8d..28ade8d1 100644 --- a/samples/modular-guava-maven/README.md +++ b/samples/modular-guava-maven/README.md @@ -1,6 +1,7 @@ # Modular Guava (Maven) -This sample builds a Guava-dependent pure-Java app [with the JPMS attic](../..). The app's module is `demo.modularguava`: +This sample builds a Guava-dependent pure-Java app [with the JPMS attic](../..). The app's module is +`demo.modularguava`: ```java module demo.modularguava { diff --git a/samples/modular-guava-repo/README.md b/samples/modular-guava-repo/README.md index 910b2c6c..570fd3b2 100644 --- a/samples/modular-guava-repo/README.md +++ b/samples/modular-guava-repo/README.md @@ -1,3 +1,4 @@ ## Modular Guava -Sample pure-Java Gradle project which uses the libraries provided by this repository in order to build a fully modular Java app that makes use of Guava. +Sample pure-Java Gradle project which uses the libraries provided by this repository in order to build a fully modular +Java app that makes use of Guava. diff --git a/samples/modular-guava/README.md b/samples/modular-guava/README.md index 910b2c6c..570fd3b2 100644 --- a/samples/modular-guava/README.md +++ b/samples/modular-guava/README.md @@ -1,3 +1,4 @@ ## Modular Guava -Sample pure-Java Gradle project which uses the libraries provided by this repository in order to build a fully modular Java app that makes use of Guava. +Sample pure-Java Gradle project which uses the libraries provided by this repository in order to build a fully modular +Java app that makes use of Guava. diff --git a/samples/modular-proto/README.md b/samples/modular-proto/README.md index 72f69ebb..b33455f3 100644 --- a/samples/modular-proto/README.md +++ b/samples/modular-proto/README.md @@ -1,3 +1,4 @@ ## Modular Protobuf -Sample pure-Java Gradle project which uses the libraries provided by this repository in order to build a fully modular Java app that makes use of Guava and Protocol Buffers. +Sample pure-Java Gradle project which uses the libraries provided by this repository in order to build a fully modular +Java app that makes use of Guava and Protocol Buffers. diff --git a/socket.yml b/socket.yml new file mode 100644 index 00000000..54286c3b --- /dev/null +++ b/socket.yml @@ -0,0 +1,2 @@ +issueRules: + binScriptConfusion: false