diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..ea32665 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,56 @@ +name: Coverage + +on: + pull_request: + push: + branches: + - master + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + coverage: + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout Repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Set Up Java 17 for Compatibility Tests + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 17 + + - name: Save Java 17 Toolchain + run: echo "EMBED_CODE_JAVA_17_HOME=$JAVA_HOME" >> "$GITHUB_ENV" + + - name: Set Up Java 25 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 25 + + - name: Set Up Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Generate Test Coverage + run: >- + ./gradlew + -Dorg.gradle.java.installations.paths="$EMBED_CODE_JAVA_17_HOME" + --no-configuration-cache + :gradle-plugin:jacocoTestReport + + - name: Upload Coverage + uses: codecov/codecov-action@v7 + with: + files: ./gradle-plugin/build/reports/jacoco/test/jacocoTestReport.xml + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/PROJECT.md b/PROJECT.md index 46592ac..1238bcc 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -38,6 +38,8 @@ Embed Code configuration file and do not need to install the executable or Kotli - `config/detekt/`: project-specific Detekt rules and the baseline for existing findings. - `scripts/check_agent_config.py` and `scripts/tests/`: deterministic validation and tests. - `.github/workflows/check.yml`: agent configuration and Ubuntu and Windows build verification. +- `.github/workflows/coverage.yml`: JaCoCo report generation and Codecov upload, using the patch + target defined in `codecov.yml`. - `.claude/commands/`: thin Claude slash-command entry points for deliberate workflows. - `.agents/guidelines/`: shared writing, English-language, and project-ownership rules. - `.agents/skills/`: repository engineering, test, writing, review, and security workflows. @@ -87,6 +89,10 @@ version numbers into agent guidance where a durable source path is sufficient. - Keep tests offline with deterministic fixtures, temporary directories, and loopback HTTP servers. - Use `./gradlew test` for unit tests, `./gradlew functionalTest` for TestKit tests, and `./gradlew check` for both plus plugin validation. +- Use `./gradlew --no-configuration-cache :gradle-plugin:jacocoTestReport` to combine unit and + TestKit coverage into Codecov XML. The coverage-only TestKit task instruments forked builds and + does not support the configuration cache; the ordinary functional suite still verifies + configuration-cache reuse. - Use `./gradlew :buildSrc:check` for the build-logic tests and Detekt analysis. `buildSrc` is a separate build, so the root `check` task does not reach them; CI runs the task explicitly. - Use `./gradlew generateDependencyReports` after changing dependencies or the plugin version. diff --git a/README.md b/README.md index ca008df..8f97fa7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ [![Build on Ubuntu and Windows][build-badge]][gh-actions] +[![Codecov][coverage-badge]][codecov] [![license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0) # Embed Code Gradle plugin @@ -178,5 +179,7 @@ consuming project's `settings.gradle.kts`. Adding it only to the regular The plugin is available under the [Apache License 2.0](LICENSE). [build-badge]: https://github.com/SpineEventEngine/embed-code-gradle-plugin/actions/workflows/check.yml/badge.svg +[codecov]: https://codecov.io/gh/SpineEventEngine/embed-code-gradle-plugin +[coverage-badge]: https://codecov.io/gh/SpineEventEngine/embed-code-gradle-plugin/graph/badge.svg [embed-code]: https://github.com/SpineEventEngine/embed-code-go [gh-actions]: https://github.com/SpineEventEngine/embed-code-gradle-plugin/actions diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..5a9052a --- /dev/null +++ b/codecov.yml @@ -0,0 +1,10 @@ +coverage: + status: + project: + default: + informational: true + patch: + default: + target: 90% + threshold: 0% + only_pulls: true diff --git a/dependencies.md b/dependencies.md index 5997961..55d4a2f 100644 --- a/dependencies.md +++ b/dependencies.md @@ -209,98 +209,139 @@ > - **POM Project URL**: [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) > - **POM License**: Apache-2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) +## BSD-3-Clause + +**48** **Group:** `org.ow2.asm` **Name:** `asm` **Version:** `9.9` +> - **Manifest Project URL**: [http://asm.ow2.org](http://asm.ow2.org) +> - **Manifest License**: `https://asm.ow2.io/LICENSE.txt` +> - **POM Project URL**: [http://asm.ow2.io/](http://asm.ow2.io/) +> - **POM License**: BSD-3-Clause - [https://asm.ow2.io/license.html](https://asm.ow2.io/license.html) +> - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) + +**49** **Group:** `org.ow2.asm` **Name:** `asm-commons` **Version:** `9.9` +> - **Manifest Project URL**: [http://asm.ow2.org](http://asm.ow2.org) +> - **Manifest License**: `https://asm.ow2.io/LICENSE.txt` +> - **POM Project URL**: [http://asm.ow2.io/](http://asm.ow2.io/) +> - **POM License**: BSD-3-Clause - [https://asm.ow2.io/license.html](https://asm.ow2.io/license.html) +> - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) + +**50** **Group:** `org.ow2.asm` **Name:** `asm-tree` **Version:** `9.9` +> - **Manifest Project URL**: [http://asm.ow2.org](http://asm.ow2.org) +> - **Manifest License**: `https://asm.ow2.io/LICENSE.txt` +> - **POM Project URL**: [http://asm.ow2.io/](http://asm.ow2.io/) +> - **POM License**: BSD-3-Clause - [https://asm.ow2.io/license.html](https://asm.ow2.io/license.html) +> - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) + +## EPL-2.0 + +**51** **Group:** `org.jacoco` **Name:** `org.jacoco.agent` **Version:** `0.8.14` +> - **POM License**: EPL-2.0 - [https://www.eclipse.org/legal/epl-2.0/](https://www.eclipse.org/legal/epl-2.0/) +> - **Embedded license files**: `org.jacoco.agent-0.8.14.jar/about.html` + +**52** **Group:** `org.jacoco` **Name:** `org.jacoco.ant` **Version:** `0.8.14` +> - **POM License**: EPL-2.0 - [https://www.eclipse.org/legal/epl-2.0/](https://www.eclipse.org/legal/epl-2.0/) +> - **Embedded license files**: `org.jacoco.ant-0.8.14.jar/about.html` + +**53** **Group:** `org.jacoco` **Name:** `org.jacoco.core` **Version:** `0.8.14` +> - **POM License**: EPL-2.0 - [https://www.eclipse.org/legal/epl-2.0/](https://www.eclipse.org/legal/epl-2.0/) +> - **Embedded license files**: `org.jacoco.core-0.8.14.jar/about.html` + +**54** **Group:** `org.jacoco` **Name:** `org.jacoco.report` **Version:** `0.8.14` +> - **POM License**: EPL-2.0 - [https://www.eclipse.org/legal/epl-2.0/](https://www.eclipse.org/legal/epl-2.0/) +> - **Embedded license files**: `org.jacoco.report-0.8.14.jar/about.html` + ## Eclipse Public License v2.0 -**48** **Group:** `org.junit` **Name:** `junit-bom` **Version:** `6.1.2` +**55** **Group:** `org.junit` **Name:** `junit-bom` **Version:** `6.1.2` > - **POM Project URL**: [https://junit.org/](https://junit.org/) > - **POM License**: Eclipse Public License v2.0 - [https://www.eclipse.org/legal/epl-v20.html](https://www.eclipse.org/legal/epl-v20.html) -**49** **Group:** `org.junit.jupiter` **Name:** `junit-jupiter` **Version:** `6.1.2` +**56** **Group:** `org.junit.jupiter` **Name:** `junit-jupiter` **Version:** `6.1.2` > - **POM Project URL**: [https://junit.org/](https://junit.org/) > - **POM License**: Eclipse Public License v2.0 - [https://www.eclipse.org/legal/epl-v20.html](https://www.eclipse.org/legal/epl-v20.html) > - **Embedded license files**: `junit-jupiter-6.1.2.jar/META-INF/LICENSE.md` -**50** **Group:** `org.junit.jupiter` **Name:** `junit-jupiter-api` **Version:** `6.1.2` +**57** **Group:** `org.junit.jupiter` **Name:** `junit-jupiter-api` **Version:** `6.1.2` > - **POM Project URL**: [https://junit.org/](https://junit.org/) > - **POM License**: Eclipse Public License v2.0 - [https://www.eclipse.org/legal/epl-v20.html](https://www.eclipse.org/legal/epl-v20.html) > - **Embedded license files**: `junit-jupiter-api-6.1.2.jar/META-INF/LICENSE.md` -**51** **Group:** `org.junit.jupiter` **Name:** `junit-jupiter-engine` **Version:** `6.1.2` +**58** **Group:** `org.junit.jupiter` **Name:** `junit-jupiter-engine` **Version:** `6.1.2` > - **POM Project URL**: [https://junit.org/](https://junit.org/) > - **POM License**: Eclipse Public License v2.0 - [https://www.eclipse.org/legal/epl-v20.html](https://www.eclipse.org/legal/epl-v20.html) > - **Embedded license files**: `junit-jupiter-engine-6.1.2.jar/META-INF/LICENSE.md` -**52** **Group:** `org.junit.jupiter` **Name:** `junit-jupiter-params` **Version:** `6.1.2` +**59** **Group:** `org.junit.jupiter` **Name:** `junit-jupiter-params` **Version:** `6.1.2` > - **POM Project URL**: [https://junit.org/](https://junit.org/) > - **POM License**: Eclipse Public License v2.0 - [https://www.eclipse.org/legal/epl-v20.html](https://www.eclipse.org/legal/epl-v20.html) > - **Embedded license files**: `junit-jupiter-params-6.1.2.jar/META-INF/LICENSE.md` -**53** **Group:** `org.junit.platform` **Name:** `junit-platform-commons` **Version:** `6.1.2` +**60** **Group:** `org.junit.platform` **Name:** `junit-platform-commons` **Version:** `6.1.2` > - **POM Project URL**: [https://junit.org/](https://junit.org/) > - **POM License**: Eclipse Public License v2.0 - [https://www.eclipse.org/legal/epl-v20.html](https://www.eclipse.org/legal/epl-v20.html) > - **Embedded license files**: `junit-platform-commons-6.1.2.jar/META-INF/LICENSE.md` -**54** **Group:** `org.junit.platform` **Name:** `junit-platform-engine` **Version:** `6.1.2` +**61** **Group:** `org.junit.platform` **Name:** `junit-platform-engine` **Version:** `6.1.2` > - **POM Project URL**: [https://junit.org/](https://junit.org/) > - **POM License**: Eclipse Public License v2.0 - [https://www.eclipse.org/legal/epl-v20.html](https://www.eclipse.org/legal/epl-v20.html) > - **Embedded license files**: `junit-platform-engine-6.1.2.jar/META-INF/LICENSE.md` -**55** **Group:** `org.junit.platform` **Name:** `junit-platform-launcher` **Version:** `6.1.2` +**62** **Group:** `org.junit.platform` **Name:** `junit-platform-launcher` **Version:** `6.1.2` > - **POM Project URL**: [https://junit.org/](https://junit.org/) > - **POM License**: Eclipse Public License v2.0 - [https://www.eclipse.org/legal/epl-v20.html](https://www.eclipse.org/legal/epl-v20.html) > - **Embedded license files**: `junit-platform-launcher-6.1.2.jar/META-INF/LICENSE.md` ## The Apache License, Version 2.0 -**56** **Group:** `com.fasterxml.woodstox` **Name:** `woodstox-core` **Version:** `6.5.1` +**63** **Group:** `com.fasterxml.woodstox` **Name:** `woodstox-core` **Version:** `6.5.1` > - **Project URL**: [https://github.com/FasterXML/woodstox](https://github.com/FasterXML/woodstox) > - **POM License**: The Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **Embedded license files**: `woodstox-core-6.5.1.jar/META-INF/LICENSE` -**57** **Group:** `org.apiguardian` **Name:** `apiguardian-api` **Version:** `1.1.2` +**64** **Group:** `org.apiguardian` **Name:** `apiguardian-api` **Version:** `1.1.2` > - **Project URL**: [https://github.com/apiguardian-team/apiguardian](https://github.com/apiguardian-team/apiguardian) > - **POM License**: The Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **Embedded license files**: `apiguardian-api-1.1.2.jar/META-INF/LICENSE` -**58** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-reflect` **Version:** `1.6.10` +**65** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-reflect` **Version:** `1.6.10` > - **POM Project URL**: [https://kotlinlang.org/](https://kotlinlang.org/) > - **POM License**: The Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**59** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-reflect` **Version:** `2.0.21` +**66** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-reflect` **Version:** `2.0.21` > - **POM Project URL**: [https://kotlinlang.org/](https://kotlinlang.org/) > - **POM License**: The Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**60** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib` **Version:** `2.0.21` +**67** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib` **Version:** `2.0.21` > - **POM Project URL**: [https://kotlinlang.org/](https://kotlinlang.org/) > - **POM License**: The Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**61** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib-common` **Version:** `2.0.21` +**68** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib-common` **Version:** `2.0.21` > - **POM Project URL**: [https://kotlinlang.org/](https://kotlinlang.org/) > - **POM License**: The Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**62** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib-jdk7` **Version:** `1.8.20` +**69** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib-jdk7` **Version:** `1.8.20` > - **POM Project URL**: [https://kotlinlang.org/](https://kotlinlang.org/) > - **POM License**: The Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**63** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib-jdk8` **Version:** `1.8.20` +**70** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib-jdk8` **Version:** `1.8.20` > - **POM Project URL**: [https://kotlinlang.org/](https://kotlinlang.org/) > - **POM License**: The Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**64** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-html-jvm` **Version:** `0.12.0` +**71** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-html-jvm` **Version:** `0.12.0` > - **POM Project URL**: [https://github.com/Kotlin/kotlinx.html](https://github.com/Kotlin/kotlinx.html) > - **POM License**: The Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**65** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-html-jvm` **Version:** `0.9.1` +**72** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-html-jvm` **Version:** `0.9.1` > - **POM Project URL**: [https://github.com/Kotlin/kotlinx.html](https://github.com/Kotlin/kotlinx.html) > - **POM License**: The Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**66** **Group:** `org.jspecify` **Name:** `jspecify` **Version:** `1.0.0` +**73** **Group:** `org.jspecify` **Name:** `jspecify` **Version:** `1.0.0` > - **Manifest Project URL**: [https://jspecify.dev/docs/start-here](https://jspecify.dev/docs/start-here) > - **POM Project URL**: [http://jspecify.org/](http://jspecify.org/) > - **POM License**: The Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**67** **Group:** `org.opentest4j` **Name:** `opentest4j` **Version:** `1.3.0` +**74** **Group:** `org.opentest4j` **Name:** `opentest4j` **Version:** `1.3.0` > - **Manifest License**: The Apache License, Version 2.0 > - **POM Project URL**: [https://github.com/ota4j-team/opentest4j](https://github.com/ota4j-team/opentest4j) > - **POM License**: The Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -308,186 +349,186 @@ ## The Apache Software License, Version 2.0 -**68** **Group:** `com.fasterxml.jackson` **Name:** `jackson-bom` **Version:** `2.15.3` +**75** **Group:** `com.fasterxml.jackson` **Name:** `jackson-bom` **Version:** `2.15.3` > - **POM Project URL**: [https://github.com/FasterXML/jackson-bom](https://github.com/FasterXML/jackson-bom) > - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**69** **Group:** `com.fasterxml.jackson.core` **Name:** `jackson-annotations` **Version:** `2.15.3` +**76** **Group:** `com.fasterxml.jackson.core` **Name:** `jackson-annotations` **Version:** `2.15.3` > - **Project URL**: [https://github.com/FasterXML/jackson](https://github.com/FasterXML/jackson) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) > - **Embedded license files**: `jackson-annotations-2.15.3.jar/META-INF/LICENSE` - `jackson-annotations-2.15.3.jar/META-INF/NOTICE` -**70** **Group:** `com.fasterxml.jackson.core` **Name:** `jackson-core` **Version:** `2.15.3` +**77** **Group:** `com.fasterxml.jackson.core` **Name:** `jackson-core` **Version:** `2.15.3` > - **Project URL**: [https://github.com/FasterXML/jackson-core](https://github.com/FasterXML/jackson-core) > - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) > - **Embedded license files**: `jackson-core-2.15.3.jar/META-INF/LICENSE` - `jackson-core-2.15.3.jar/META-INF/NOTICE` -**71** **Group:** `com.fasterxml.jackson.core` **Name:** `jackson-databind` **Version:** `2.15.3` +**78** **Group:** `com.fasterxml.jackson.core` **Name:** `jackson-databind` **Version:** `2.15.3` > - **Project URL**: [https://github.com/FasterXML/jackson](https://github.com/FasterXML/jackson) > - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) > - **Embedded license files**: `jackson-databind-2.15.3.jar/META-INF/LICENSE` - `jackson-databind-2.15.3.jar/META-INF/NOTICE` -**72** **Group:** `com.fasterxml.jackson.dataformat` **Name:** `jackson-dataformat-xml` **Version:** `2.15.3` +**79** **Group:** `com.fasterxml.jackson.dataformat` **Name:** `jackson-dataformat-xml` **Version:** `2.15.3` > - **Project URL**: [https://github.com/FasterXML/jackson-dataformat-xml](https://github.com/FasterXML/jackson-dataformat-xml) > - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **Embedded license files**: `jackson-dataformat-xml-2.15.3.jar/META-INF/LICENSE` - `jackson-dataformat-xml-2.15.3.jar/META-INF/NOTICE` -**73** **Group:** `com.fasterxml.jackson.module` **Name:** `jackson-module-kotlin` **Version:** `2.15.3` +**80** **Group:** `com.fasterxml.jackson.module` **Name:** `jackson-module-kotlin` **Version:** `2.15.3` > - **Project URL**: [https://github.com/FasterXML/jackson-module-kotlin](https://github.com/FasterXML/jackson-module-kotlin) > - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) > - **Embedded license files**: `jackson-module-kotlin-2.15.3.jar/META-INF/LICENSE` - `jackson-module-kotlin-2.15.3.jar/META-INF/NOTICE` -**74** **Group:** `com.fasterxml.woodstox` **Name:** `woodstox-core` **Version:** `6.5.1` +**81** **Group:** `com.fasterxml.woodstox` **Name:** `woodstox-core` **Version:** `6.5.1` > - **Project URL**: [https://github.com/FasterXML/woodstox](https://github.com/FasterXML/woodstox) > - **POM License**: The Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **Embedded license files**: `woodstox-core-6.5.1.jar/META-INF/LICENSE` -**75** **Group:** `dev.detekt` **Name:** `detekt-api` **Version:** `2.0.0-alpha.5` +**82** **Group:** `dev.detekt` **Name:** `detekt-api` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**76** **Group:** `dev.detekt` **Name:** `detekt-cli` **Version:** `2.0.0-alpha.5` +**83** **Group:** `dev.detekt` **Name:** `detekt-cli` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**77** **Group:** `dev.detekt` **Name:** `detekt-core` **Version:** `2.0.0-alpha.5` +**84** **Group:** `dev.detekt` **Name:** `detekt-core` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**78** **Group:** `dev.detekt` **Name:** `detekt-kotlin-analysis-api` **Version:** `2.0.0-alpha.5` +**85** **Group:** `dev.detekt` **Name:** `detekt-kotlin-analysis-api` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**79** **Group:** `dev.detekt` **Name:** `detekt-kotlin-analysis-api-standalone` **Version:** `2.0.0-alpha.5` +**86** **Group:** `dev.detekt` **Name:** `detekt-kotlin-analysis-api-standalone` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**80** **Group:** `dev.detekt` **Name:** `detekt-metrics` **Version:** `2.0.0-alpha.5` +**87** **Group:** `dev.detekt` **Name:** `detekt-metrics` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**81** **Group:** `dev.detekt` **Name:** `detekt-parser` **Version:** `2.0.0-alpha.5` +**88** **Group:** `dev.detekt` **Name:** `detekt-parser` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**82** **Group:** `dev.detekt` **Name:** `detekt-psi-utils` **Version:** `2.0.0-alpha.5` +**89** **Group:** `dev.detekt` **Name:** `detekt-psi-utils` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**83** **Group:** `dev.detekt` **Name:** `detekt-report-checkstyle` **Version:** `2.0.0-alpha.5` +**90** **Group:** `dev.detekt` **Name:** `detekt-report-checkstyle` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**84** **Group:** `dev.detekt` **Name:** `detekt-report-html` **Version:** `2.0.0-alpha.5` +**91** **Group:** `dev.detekt` **Name:** `detekt-report-html` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**85** **Group:** `dev.detekt` **Name:** `detekt-report-markdown` **Version:** `2.0.0-alpha.5` +**92** **Group:** `dev.detekt` **Name:** `detekt-report-markdown` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**86** **Group:** `dev.detekt` **Name:** `detekt-report-sarif` **Version:** `2.0.0-alpha.5` +**93** **Group:** `dev.detekt` **Name:** `detekt-report-sarif` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**87** **Group:** `dev.detekt` **Name:** `detekt-rules` **Version:** `2.0.0-alpha.5` +**94** **Group:** `dev.detekt` **Name:** `detekt-rules` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**88** **Group:** `dev.detekt` **Name:** `detekt-rules-comments` **Version:** `2.0.0-alpha.5` +**95** **Group:** `dev.detekt` **Name:** `detekt-rules-comments` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**89** **Group:** `dev.detekt` **Name:** `detekt-rules-complexity` **Version:** `2.0.0-alpha.5` +**96** **Group:** `dev.detekt` **Name:** `detekt-rules-complexity` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**90** **Group:** `dev.detekt` **Name:** `detekt-rules-coroutines` **Version:** `2.0.0-alpha.5` +**97** **Group:** `dev.detekt` **Name:** `detekt-rules-coroutines` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**91** **Group:** `dev.detekt` **Name:** `detekt-rules-empty-blocks` **Version:** `2.0.0-alpha.5` +**98** **Group:** `dev.detekt` **Name:** `detekt-rules-empty-blocks` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**92** **Group:** `dev.detekt` **Name:** `detekt-rules-exceptions` **Version:** `2.0.0-alpha.5` +**99** **Group:** `dev.detekt` **Name:** `detekt-rules-exceptions` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**93** **Group:** `dev.detekt` **Name:** `detekt-rules-naming` **Version:** `2.0.0-alpha.5` +**100** **Group:** `dev.detekt` **Name:** `detekt-rules-naming` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**94** **Group:** `dev.detekt` **Name:** `detekt-rules-performance` **Version:** `2.0.0-alpha.5` +**101** **Group:** `dev.detekt` **Name:** `detekt-rules-performance` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**95** **Group:** `dev.detekt` **Name:** `detekt-rules-potential-bugs` **Version:** `2.0.0-alpha.5` +**102** **Group:** `dev.detekt` **Name:** `detekt-rules-potential-bugs` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**96** **Group:** `dev.detekt` **Name:** `detekt-rules-style` **Version:** `2.0.0-alpha.5` +**103** **Group:** `dev.detekt` **Name:** `detekt-rules-style` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**97** **Group:** `dev.detekt` **Name:** `detekt-tooling` **Version:** `2.0.0-alpha.5` +**104** **Group:** `dev.detekt` **Name:** `detekt-tooling` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**98** **Group:** `dev.detekt` **Name:** `detekt-utils` **Version:** `2.0.0-alpha.5` +**105** **Group:** `dev.detekt` **Name:** `detekt-utils` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**99** **Group:** `dev.drewhamilton.poko` **Name:** `poko-annotations` **Version:** `0.23.1` +**106** **Group:** `dev.drewhamilton.poko` **Name:** `poko-annotations` **Version:** `0.23.1` > - **POM Project URL**: [https://github.com/drewhamilton/Poko](https://github.com/drewhamilton/Poko) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**100** **Group:** `dev.drewhamilton.poko` **Name:** `poko-annotations-jvm` **Version:** `0.23.1` +**107** **Group:** `dev.drewhamilton.poko` **Name:** `poko-annotations-jvm` **Version:** `0.23.1` > - **POM Project URL**: [https://github.com/drewhamilton/Poko](https://github.com/drewhamilton/Poko) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**101** **Group:** `io.github.detekt.sarif4k` **Name:** `sarif4k` **Version:** `0.6.0` +**108** **Group:** `io.github.detekt.sarif4k` **Name:** `sarif4k` **Version:** `0.6.0` > - **POM Project URL**: [https://detekt.github.io/detekt](https://detekt.github.io/detekt) > - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**102** **Group:** `io.github.detekt.sarif4k` **Name:** `sarif4k-jvm` **Version:** `0.6.0` +**109** **Group:** `io.github.detekt.sarif4k` **Name:** `sarif4k-jvm` **Version:** `0.6.0` > - **POM Project URL**: [https://detekt.github.io/detekt](https://detekt.github.io/detekt) > - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**103** **Group:** `org.codehaus.woodstox` **Name:** `stax2-api` **Version:** `4.2.1` +**110** **Group:** `org.codehaus.woodstox` **Name:** `stax2-api` **Version:** `4.2.1` > - **Project URL**: [http://github.com/FasterXML/stax2-api](http://github.com/FasterXML/stax2-api) > - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **POM License**: The BSD License - [http://www.opensource.org/licenses/bsd-license.php](http://www.opensource.org/licenses/bsd-license.php) > - **Embedded license files**: `stax2-api-4.2.1.jar/META-INF/LICENSE` -**104** **Group:** `org.jetbrains` **Name:** `annotations` **Version:** `13.0` +**111** **Group:** `org.jetbrains` **Name:** `annotations` **Version:** `13.0` > - **POM Project URL**: [http://www.jetbrains.org](http://www.jetbrains.org) > - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**105** **Group:** `org.jetbrains` **Name:** `annotations` **Version:** `23.0.0` +**112** **Group:** `org.jetbrains` **Name:** `annotations` **Version:** `23.0.0` > - **POM Project URL**: [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**106** **Group:** `org.jetbrains` **Name:** `markdown` **Version:** `0.7.3` +**113** **Group:** `org.jetbrains` **Name:** `markdown` **Version:** `0.7.3` > - **POM Project URL**: [https://github.com/JetBrains/markdown](https://github.com/JetBrains/markdown) > - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**107** **Group:** `org.jetbrains` **Name:** `markdown-jvm` **Version:** `0.7.3` +**114** **Group:** `org.jetbrains` **Name:** `markdown-jvm` **Version:** `0.7.3` > - **POM Project URL**: [https://github.com/JetBrains/markdown](https://github.com/JetBrains/markdown) > - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**108** **Group:** `org.jetbrains.dokka` **Name:** `analysis-kotlin-symbols` **Version:** `2.2.0` +**115** **Group:** `org.jetbrains.dokka` **Name:** `analysis-kotlin-symbols` **Version:** `2.2.0` > - **POM Project URL**: [https://github.com/Kotlin/dokka](https://github.com/Kotlin/dokka) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) > - **Embedded license files**: `analysis-kotlin-symbols-2.2.0.jar/META-INF/LICENSE` @@ -497,45 +538,66 @@ - `analysis-kotlin-symbols-2.2.0.jar/META-INF/README.md` - `analysis-kotlin-symbols-2.2.0.jar/org/apache/log4j/NOTICE` -**109** **Group:** `org.jetbrains.dokka` **Name:** `analysis-markdown` **Version:** `2.2.0` +**116** **Group:** `org.jetbrains.dokka` **Name:** `analysis-markdown` **Version:** `2.2.0` > - **POM Project URL**: [https://github.com/Kotlin/dokka](https://github.com/Kotlin/dokka) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**110** **Group:** `org.jetbrains.dokka` **Name:** `dokka-base` **Version:** `2.2.0` +**117** **Group:** `org.jetbrains.dokka` **Name:** `dokka-base` **Version:** `2.2.0` > - **POM Project URL**: [https://github.com/Kotlin/dokka](https://github.com/Kotlin/dokka) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**111** **Group:** `org.jetbrains.dokka` **Name:** `dokka-core` **Version:** `2.2.0` +**118** **Group:** `org.jetbrains.dokka` **Name:** `dokka-core` **Version:** `2.2.0` > - **POM Project URL**: [https://github.com/Kotlin/dokka](https://github.com/Kotlin/dokka) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**112** **Group:** `org.jetbrains.dokka` **Name:** `templating-plugin` **Version:** `2.2.0` +**119** **Group:** `org.jetbrains.dokka` **Name:** `templating-plugin` **Version:** `2.2.0` > - **POM Project URL**: [https://github.com/Kotlin/dokka](https://github.com/Kotlin/dokka) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**113** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-coroutines-bom` **Version:** `1.7.3` +**120** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-coroutines-bom` **Version:** `1.7.3` > - **POM Project URL**: [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**114** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-coroutines-bom` **Version:** `1.8.0` +**121** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-coroutines-bom` **Version:** `1.8.0` > - **POM Project URL**: [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**115** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-coroutines-core` **Version:** `1.7.3` +**122** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-coroutines-core` **Version:** `1.7.3` > - **POM Project URL**: [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**116** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-coroutines-core-jvm` **Version:** `1.7.3` +**123** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-coroutines-core-jvm` **Version:** `1.7.3` > - **POM Project URL**: [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**117** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-coroutines-core-jvm` **Version:** `1.8.0` +**124** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-coroutines-core-jvm` **Version:** `1.8.0` > - **POM Project URL**: [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) +**125** **Group:** `org.ow2.asm` **Name:** `asm` **Version:** `9.9` +> - **Manifest Project URL**: [http://asm.ow2.org](http://asm.ow2.org) +> - **Manifest License**: `https://asm.ow2.io/LICENSE.txt` +> - **POM Project URL**: [http://asm.ow2.io/](http://asm.ow2.io/) +> - **POM License**: BSD-3-Clause - [https://asm.ow2.io/license.html](https://asm.ow2.io/license.html) +> - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) + +**126** **Group:** `org.ow2.asm` **Name:** `asm-commons` **Version:** `9.9` +> - **Manifest Project URL**: [http://asm.ow2.org](http://asm.ow2.org) +> - **Manifest License**: `https://asm.ow2.io/LICENSE.txt` +> - **POM Project URL**: [http://asm.ow2.io/](http://asm.ow2.io/) +> - **POM License**: BSD-3-Clause - [https://asm.ow2.io/license.html](https://asm.ow2.io/license.html) +> - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) + +**127** **Group:** `org.ow2.asm` **Name:** `asm-tree` **Version:** `9.9` +> - **Manifest Project URL**: [http://asm.ow2.org](http://asm.ow2.org) +> - **Manifest License**: `https://asm.ow2.io/LICENSE.txt` +> - **POM Project URL**: [http://asm.ow2.io/](http://asm.ow2.io/) +> - **POM License**: BSD-3-Clause - [https://asm.ow2.io/license.html](https://asm.ow2.io/license.html) +> - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) + ## The BSD License -**118** **Group:** `org.codehaus.woodstox` **Name:** `stax2-api` **Version:** `4.2.1` +**128** **Group:** `org.codehaus.woodstox` **Name:** `stax2-api` **Version:** `4.2.1` > - **Project URL**: [http://github.com/FasterXML/stax2-api](http://github.com/FasterXML/stax2-api) > - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **POM License**: The BSD License - [http://www.opensource.org/licenses/bsd-license.php](http://www.opensource.org/licenses/bsd-license.php) @@ -543,13 +605,13 @@ ## The MIT License -**119** **Group:** `org.checkerframework` **Name:** `checker-qual` **Version:** `3.19.0` +**129** **Group:** `org.checkerframework` **Name:** `checker-qual` **Version:** `3.19.0` > - **Manifest License**: MIT > - **POM Project URL**: [https://checkerframework.org](https://checkerframework.org) > - **POM License**: The MIT License - [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT) > - **Embedded license files**: `checker-qual-3.19.0.jar/META-INF/LICENSE.txt` -**120** **Group:** `org.jsoup` **Name:** `jsoup` **Version:** `1.16.1` +**130** **Group:** `org.jsoup` **Name:** `jsoup` **Version:** `1.16.1` > - **Project URL**: [https://jsoup.org/](https://jsoup.org/) > - **POM License**: The MIT License - [https://jsoup.org/license](https://jsoup.org/license) > - **Embedded license files**: `jsoup-1.16.1.jar/META-INF/LICENSE` diff --git a/gradle-plugin/build.gradle.kts b/gradle-plugin/build.gradle.kts index 84bf131..5b1bf07 100644 --- a/gradle-plugin/build.gradle.kts +++ b/gradle-plugin/build.gradle.kts @@ -34,14 +34,17 @@ import io.spine.embedcode.gradle.report.DependencyMarkdownReportRenderer import io.spine.embedcode.gradle.report.GenerateDependencyPom import io.spine.embedcode.gradle.report.UpdateDependencyReports import org.apache.tools.ant.filters.ReplaceTokens +import org.gradle.api.Action import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.tasks.Sync import org.gradle.plugin.compatibility.compatibility +import org.gradle.testing.jacoco.plugins.JacocoTaskExtension plugins { id("jvm-module") id("dokka-configuration") `java-gradle-plugin` + jacoco `maven-publish` } @@ -98,9 +101,7 @@ configurations[functionalTestSourceSet.runtimeOnlyConfigurationName].extendsFrom configurations.testRuntimeOnly.get(), ) -val functionalTest = tasks.register("functionalTest") { - description = "Runs TestKit functional tests." - group = LifecycleBasePlugin.VERIFICATION_GROUP +val configureFunctionalTest = Action { testClassesDirs = functionalTestSourceSet.output.classesDirs classpath = functionalTestSourceSet.runtimeClasspath useJUnitPlatform() @@ -113,10 +114,66 @@ val functionalTest = tasks.register("functionalTest") { shouldRunAfter(tasks.test) } +val functionalTest = tasks.register("functionalTest") { + configureFunctionalTest.execute(this) + description = "Runs TestKit functional tests." + group = LifecycleBasePlugin.VERIFICATION_GROUP + extensions.configure { + isEnabled = false + } +} + +val testKitCoverageData = layout.buildDirectory.file("jacoco/testKit.exec") +val coverageFunctionalTest = tasks.register("coverageFunctionalTest") { + configureFunctionalTest.execute(this) + description = "Runs TestKit functional tests and collects plugin coverage." + group = LifecycleBasePlugin.VERIFICATION_GROUP + extensions.configure { + isEnabled = false + setDestinationFile(testKitCoverageData.map { file -> file.asFile }) + } + outputs.file(testKitCoverageData) + notCompatibleWithConfigurationCache( + "JaCoCo coverage of forked TestKit builds requires execution-time agent paths.", + ) + doFirst { + val coverageDataFile = testKitCoverageData.get().asFile + coverageDataFile.delete() + val jacocoExtension = extensions.getByType() + // TestKit daemons use another working directory, so JaCoCo needs an absolute destination. + val childJvmArgument = + Regex("""destfile=[^,]+""").replaceFirst( + jacocoExtension.asJvmArg, + Regex.escapeReplacement("destfile=${coverageDataFile.absolutePath}"), + ) + systemProperty( + "io.spine.embedcode.gradle.testkit.coverage.jvm-argument", + childJvmArgument, + ) + } +} + tasks.check { dependsOn(functionalTest) } +tasks.jacocoTestReport { + dependsOn(tasks.test, coverageFunctionalTest) + executionData( + tasks.test.map { task -> + task.extensions.getByType().destinationFile + }, + coverageFunctionalTest.map { task -> + task.extensions.getByType().destinationFile + }, + ) + reports { + xml.required.set(true) + html.required.set(false) + csv.required.set(false) + } +} + base { archivesName.set("embed-code-gradle-plugin") } diff --git a/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt b/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt index ebb5f3b..5122e4f 100644 --- a/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt +++ b/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt @@ -111,9 +111,9 @@ internal class EmbedCodePluginSpec { @Test @EnabledOnOs(OS.LINUX, OS.MAC) fun `reuse the configuration cache`() { - runner(":checkEmbedding").build() + runner(":checkEmbedding", collectCoverage = false).build() - val result = runner(":checkEmbedding").build() + val result = runner(":checkEmbedding", collectCoverage = false).build() result.output shouldContain "Reusing configuration cache." result.task(":installEmbedCode")?.outcome shouldBe TaskOutcome.SUCCESS @@ -150,6 +150,53 @@ internal class EmbedCodePluginSpec { Files.readString(installedExecutable(releaseTag)) shouldBe "Linux executable" } + @Test + fun `install a Linux ZIP release with a nested executable`() { + val releaseTag = "v1.2.5-nested-executable" + val asset = releaseDirectory.resolve( + "download/$releaseTag/embed-code-linux.zip", + ) + Files.createDirectories(asset.parent) + ZipOutputStream(Files.newOutputStream(asset)).use { zip -> + zip.putNextEntry(ZipEntry("bin/embed-code-linux")) + zip.write("Nested Linux executable".toByteArray()) + zip.closeEntry() + } + writeBuildFile(version = releaseTag, sha256 = sha256(asset)) + selectLinuxReleaseAsset() + + val result = runner(":installEmbedCode").build() + + result.task(":installEmbedCode")?.outcome shouldBe TaskOutcome.SUCCESS + Files.readString(installedExecutable(releaseTag)) shouldBe "Nested Linux executable" + } + + @Test + fun `reject a Linux ZIP release without the expected executable`() { + val releaseTag = "v1.2.5-missing-executable" + val asset = releaseDirectory.resolve( + "download/$releaseTag/embed-code-linux.zip", + ) + Files.createDirectories(asset.parent) + ZipOutputStream(Files.newOutputStream(asset)).use { zip -> + zip.putNextEntry(ZipEntry("README.md")) + zip.write("No executable in this archive".toByteArray()) + zip.closeEntry() + } + writeBuildFile(version = releaseTag, sha256 = sha256(asset)) + selectLinuxReleaseAsset() + + val result = runner(":installEmbedCode").buildAndFail() + + result.task(":installEmbedCode")?.outcome shouldBe TaskOutcome.FAILED + result.output shouldContain "does not contain `embed-code-linux`" + Files.exists(installedExecutable(releaseTag)) shouldBe false + val installation = installationDirectory(releaseTag) + listOf("asset.sha256", "executable.sha256", "source.sha256").forEach { marker -> + Files.exists(installation.resolve(marker)) shouldBe false + } + } + @Test fun `install a bare Linux asset from a release published before ZIP packaging`() { val asset = releaseDirectory.resolve( @@ -349,6 +396,48 @@ internal class EmbedCodePluginSpec { Files.isExecutable(executable) shouldBe true } + @Test + fun `restore missing asset checksum metadata from the verified cached asset`() { + val downloads = AtomicInteger() + val server = startReleaseServer(downloads) + try { + writeBuildFile(downloadBaseUrl = server.releaseBaseUrl) + runner(":installEmbedCode").build() + val assetChecksum = installationDirectory().resolve("asset.sha256") + Files.delete(assetChecksum) + + val result = runner(":installEmbedCode").build() + + result.task(":installEmbedCode")?.outcome shouldBe TaskOutcome.SUCCESS + downloads.get() shouldBe 1 + Files.readString(assetChecksum).trim() shouldBe releaseAssetSha256() + result.output shouldContain "Reusing verified Embed Code" + } finally { + server.stop(0) + } + } + + @Test + fun `replace malformed executable checksum metadata from the verified cached asset`() { + val downloads = AtomicInteger() + val server = startReleaseServer(downloads) + try { + writeBuildFile(downloadBaseUrl = server.releaseBaseUrl) + runner(":installEmbedCode").build() + val executableChecksum = installationDirectory().resolve("executable.sha256") + Files.writeString(executableChecksum, "not-a-sha256") + + val result = runner(":installEmbedCode").build() + + result.task(":installEmbedCode")?.outcome shouldBe TaskOutcome.SUCCESS + downloads.get() shouldBe 1 + Files.readString(executableChecksum).trim() shouldBe sha256(installedExecutable()) + result.output shouldContain "Reusing verified Embed Code" + } finally { + server.stop(0) + } + } + @Test fun `remove staged release assets after cache reuse`() { runner(":installEmbedCode").build() @@ -686,6 +775,39 @@ internal class EmbedCodePluginSpec { Files.exists(projectDirectory.resolve("escaped/release-asset")) shouldBe false } + @Test + fun `reject a regular file in the installation path`() { + val installationRoot = projectDirectory.resolve("build/embed-code") + val blockingFile = installationRoot.resolve("blocked") + Files.writeString( + projectDirectory.resolve("build.gradle.kts"), + """ + + tasks.named("installEmbedCode") { + cachedAssetFile.set( + layout.buildDirectory.file("embed-code/blocked/release-asset") + ) + doFirst { + val blockingFile = cachedAssetFile.get().asFile.parentFile + check(blockingFile.deleteRecursively()) + blockingFile.writeText("unchanged") + } + } + """.trimIndent(), + StandardOpenOption.APPEND, + ) + + val result = runner( + ":installEmbedCode", + useConfigurationCache = false, + ).buildAndFail() + + result.output shouldContain "Embed Code installation path component" + result.output shouldContain "must be a directory." + Files.readString(blockingFile) shouldBe "unchanged" + Files.exists(installedExecutable()) shouldBe false + } + @Test @EnabledOnOs(OS.LINUX, OS.MAC) fun `reject a symbolic-link installation root`() { @@ -1048,12 +1170,29 @@ internal class EmbedCodePluginSpec { /** * Creates a runner using the plugin-under-test classpath. + * + * @param arguments Gradle tasks and options passed to the consuming build. + * @param collectCoverage whether to attach the JaCoCo agent to the consuming build. + * @param useConfigurationCache whether to enable Gradle's configuration cache. */ private fun runner( vararg arguments: String, + collectCoverage: Boolean = testKitCoverageJvmArgument != null, + useConfigurationCache: Boolean = !collectCoverage, ): GradleRunner { + require(!collectCoverage || !useConfigurationCache) { + "TestKit coverage collection is incompatible with the configuration cache." + } val gradleArguments = arguments.toMutableList() - gradleArguments.add("--configuration-cache") + if (collectCoverage) { + gradleArguments.add( + "-Dorg.gradle.jvmargs=$DEFAULT_GRADLE_DAEMON_JVM_ARGUMENTS " + + requireNotNull(testKitCoverageJvmArgument), + ) + } + if (useConfigurationCache) { + gradleArguments.add("--configuration-cache") + } gradleArguments.add("--stacktrace") return GradleRunner.create() .withProjectDir(projectDirectory.toFile()) @@ -1065,7 +1204,7 @@ internal class EmbedCodePluginSpec { * Runs check mode with [gradleVersion]. */ private fun runCheckModeWithGradle(gradleVersion: String) { - val result = runner(":checkEmbedding") + val result = runner(":checkEmbedding", collectCoverage = false) .withGradleVersion(gradleVersion) .build() @@ -1165,7 +1304,10 @@ internal class EmbedCodePluginSpec { "" } val secondSource = if (includeSecondSource) { - "namedSource(\"$secondSourceName\", layout.projectDirectory.dir(\"browser\"))" + "namedSource(" + + "\"$secondSourceName\", " + + "providers.provider { layout.projectDirectory.dir(\"browser\") }" + + ")" } else { "" } @@ -1283,7 +1425,15 @@ internal class EmbedCodePluginSpec { private val HttpServer.releaseBaseUrl: String get() = "http://127.0.0.1:${address.port}/releases" + private val testKitCoverageJvmArgument: String? + get() = System.getProperty(TEST_KIT_COVERAGE_JVM_ARGUMENT_PROPERTY) + private companion object { + const val TEST_KIT_COVERAGE_JVM_ARGUMENT_PROPERTY = + "io.spine.embedcode.gradle.testkit.coverage.jvm-argument" + // `org.gradle.jvmargs` replaces Gradle's defaults, so retain them before the JaCoCo agent. + const val DEFAULT_GRADLE_DAEMON_JVM_ARGUMENTS = + "-Xmx512m -XX:MaxMetaspaceSize=384m" val TEST_RELEASE_TAG = DEFAULT_EMBED_CODE_VERSION val TEST_RELEASE_VERSION = TEST_RELEASE_TAG.removePrefix("v") } diff --git a/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/Checksum.kt b/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/Checksum.kt index 41b53d9..ef83b43 100644 --- a/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/Checksum.kt +++ b/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/Checksum.kt @@ -28,7 +28,10 @@ package io.spine.embedcode.gradle import groovy.json.JsonSlurper import org.gradle.api.GradleException +import java.io.IOException +import java.net.HttpURLConnection import java.net.URI +import java.net.URLConnection import java.net.URLEncoder import java.nio.channels.Channels import java.nio.charset.StandardCharsets @@ -40,6 +43,8 @@ import java.security.MessageDigest import java.util.HexFormat private const val SHA256_LENGTH = 64 +private const val FIRST_SUCCESSFUL_HTTP_STATUS = 200 +private const val LAST_SUCCESSFUL_HTTP_STATUS = 299 /** * Calculates the lowercase SHA-256 digest of [file]. @@ -139,6 +144,57 @@ internal fun parseGitHubAssetSha256(json: String, assetName: String): String { return normalizeSha256(digest) } +/** + * Reads GitHub release metadata with [readMetadata] and limits token use to the GitHub API host. + */ +internal fun readGitHubReleaseMetadata( + source: URI, + githubToken: String?, + readMetadata: (URI, String?) -> String, +): String { + val token = if (source.host.equals("api.github.com", ignoreCase = true)) { + githubToken?.trim()?.ifEmpty { null } + } else { + null + } + return readMetadata(source, token) +} + +/** + * Reads UTF-8 checksum metadata from [source] without following HTTP redirects. + */ +internal fun readChecksumMetadata(source: URI, githubToken: String? = null): String { + var connection: URLConnection? = null + try { + connection = source.toURL().openConnection() + connection.connectTimeout = HTTP_CONNECT_TIMEOUT_MILLIS + connection.readTimeout = HTTP_READ_TIMEOUT_MILLIS + connection.setRequestProperty("User-Agent", "embed-code-gradle-plugin") + connection.setRequestProperty("Accept", "application/vnd.github+json") + if (githubToken != null) { + connection.setRequestProperty("Authorization", "Bearer $githubToken") + } + if (connection is HttpURLConnection) { + connection.instanceFollowRedirects = false + val status = connection.responseCode + if (status < FIRST_SUCCESSFUL_HTTP_STATUS || status > LAST_SUCCESSFUL_HTTP_STATUS) { + throw GradleException( + "Could not read checksum metadata: HTTP $status from $source.", + ) + } + } + return connection.getInputStream() + .bufferedReader(StandardCharsets.UTF_8) + .use { reader -> reader.readText() } + } catch (exception: IOException) { + throw GradleException("Could not read checksum metadata from $source.", exception) + } finally { + if (connection is HttpURLConnection) { + connection.disconnect() + } + } +} + /** * Resolves the trusted digest for a downloaded release asset. * diff --git a/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallEmbedCodeTask.kt b/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallEmbedCodeTask.kt index 15af6b1..6787472 100644 --- a/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallEmbedCodeTask.kt +++ b/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallEmbedCodeTask.kt @@ -54,6 +54,9 @@ import java.nio.file.StandardCopyOption import java.nio.file.StandardOpenOption import java.util.zip.ZipInputStream +internal const val HTTP_CONNECT_TIMEOUT_MILLIS = 30_000 +internal const val HTTP_READ_TIMEOUT_MILLIS = 120_000 + /** * Downloads and prepares the Embed Code executable selected for the host. * @@ -422,12 +425,7 @@ public abstract class InstallEmbedCodeTask : DefaultTask() { releaseTag, asset, ) { metadataSource -> - val token = if (metadataSource.host.equals("api.github.com", ignoreCase = true)) { - githubToken.orNull?.trim()?.ifEmpty { null } - } else { - null - } - readText(metadataSource, token) + readGitHubReleaseMetadata(metadataSource, githubToken.orNull, ::readChecksumMetadata) } /** @@ -488,167 +486,8 @@ public abstract class InstallEmbedCodeTask : DefaultTask() { private companion object { - const val CONNECT_TIMEOUT_MILLIS = 30_000 - const val READ_TIMEOUT_MILLIS = 120_000 const val BUFFER_SIZE = 8_192 - /** - * Detects symbolic links and directory redirects such as Windows junctions. - */ - fun isRedirectingFileSystemEntry(path: Path): Boolean { - return try { - if (Files.isSymbolicLink(path)) { - true - } else { - val parent = path.parent ?: return false - val expectedRealPath = parent.toRealPath().resolve(path.fileName).normalize() - path.toRealPath() != expectedRealPath - } - } catch (exception: IOException) { - throw GradleException( - "Could not inspect Embed Code installation path `$path`.", - exception, - ) - } - } - - /** - * Owns the normalized installation root and validates paths used by one task action. - * - * The task checks all configured paths before [prepare] creates the root. Later - * operations reuse this instance and recheck the root without repeating its creation. - */ - private class InstallationDirectory(path: Path) { - - private val root = path.toAbsolutePath().normalize() - - /** - * Normalizes [path] and verifies that it is a strict descendant of the root. - */ - fun requireInside(path: Path): Path { - val normalizedPath = path.toAbsolutePath().normalize() - if (normalizedPath == root || !normalizedPath.startsWith(root)) { - throw GradleException( - "Embed Code installation path `$normalizedPath` " + - "must remain inside `$root`.", - ) - } - return normalizedPath - } - - /** - * Creates the installation root and rejects a symlink or non-directory root. - */ - fun prepare() { - try { - if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) { - Files.createDirectories(root) - } - } catch (exception: IOException) { - throw GradleException( - "Could not create Embed Code installation directory `$root`.", - exception, - ) - } - requireRealRoot() - } - - /** - * Rejects a symbolic-link, redirecting, missing, or non-directory root. - */ - private fun requireRealRoot() { - if ( - isRedirectingFileSystemEntry(root) || - !Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS) - ) { - throw GradleException( - "Embed Code installation directory `$root` must be a real directory, " + - "not a symbolic link or redirecting entry.", - ) - } - } - - /** - * Rejects symbolic links in every existing component from the installation root. - */ - fun requireNoSymbolicLinks(path: Path) { - requireRealRoot() - val normalizedPath = requireInside(path) - var current = root - root.relativize(normalizedPath).forEach { component -> - current = current.resolve(component) - if (Files.exists(current, LinkOption.NOFOLLOW_LINKS)) { - if (isRedirectingFileSystemEntry(current)) { - throw GradleException( - "Embed Code installation path `$current` must not be a " + - "symbolic link or redirecting filesystem entry.", - ) - } - if ( - current != normalizedPath && - !Files.isDirectory(current, LinkOption.NOFOLLOW_LINKS) - ) { - throw GradleException( - "Embed Code installation path component `$current` " + - "must be a directory.", - ) - } - } - } - } - - /** - * Creates [directory] component by component without following symbolic links. - */ - fun createDirectoriesSafely(directory: Path) { - requireRealRoot() - val normalizedDirectory = directory.toAbsolutePath().normalize() - if (normalizedDirectory != root) { - requireInside(normalizedDirectory) - } - var current = root - root.relativize(normalizedDirectory).forEach { component -> - current = current.resolve(component) - if (Files.exists(current, LinkOption.NOFOLLOW_LINKS)) { - if ( - isRedirectingFileSystemEntry(current) || - !Files.isDirectory(current, LinkOption.NOFOLLOW_LINKS) - ) { - throw GradleException( - "Embed Code installation directory `$current` " + - "must be a real directory, not a symbolic link " + - "or redirecting entry.", - ) - } - } else { - try { - Files.createDirectory(current) - } catch (exception: IOException) { - throw GradleException( - "Could not create Embed Code installation directory `$current`.", - exception, - ) - } - } - } - requireRealPathInside(normalizedDirectory) - } - - /** - * Verifies that [directory] resolves below the real installation root. - */ - private fun requireRealPathInside(directory: Path) { - val realRoot = root.toRealPath() - val realDirectory = directory.toRealPath() - if (!realDirectory.startsWith(realRoot)) { - throw GradleException( - "Embed Code installation directory `$realDirectory` " + - "must remain inside `$realRoot`.", - ) - } - } - } - /** * Adds an upgrade hint when an exact tag may be missing its former automatic prefix. */ @@ -679,8 +518,8 @@ public abstract class InstallEmbedCodeTask : DefaultTask() { var connection: URLConnection? = null try { connection = source.toURL().openConnection() - connection.connectTimeout = CONNECT_TIMEOUT_MILLIS - connection.readTimeout = READ_TIMEOUT_MILLIS + connection.connectTimeout = HTTP_CONNECT_TIMEOUT_MILLIS + connection.readTimeout = HTTP_READ_TIMEOUT_MILLIS connection.setRequestProperty("User-Agent", "embed-code-gradle-plugin") if (connection is HttpURLConnection) { @@ -714,41 +553,6 @@ public abstract class InstallEmbedCodeTask : DefaultTask() { } } - /** - * Reads UTF-8 text from [source], reporting HTTP failures clearly. - */ - fun readText(source: URI, githubToken: String? = null): String { - var connection: URLConnection? = null - try { - connection = source.toURL().openConnection() - connection.connectTimeout = CONNECT_TIMEOUT_MILLIS - connection.readTimeout = READ_TIMEOUT_MILLIS - connection.setRequestProperty("User-Agent", "embed-code-gradle-plugin") - connection.setRequestProperty("Accept", "application/vnd.github+json") - if (githubToken != null) { - connection.setRequestProperty("Authorization", "Bearer $githubToken") - } - if (connection is HttpURLConnection) { - connection.instanceFollowRedirects = false - val status = connection.responseCode - if (status < 200 || status > 299) { - throw GradleException( - "Could not read checksum metadata: HTTP $status from $source.", - ) - } - } - return connection.getInputStream() - .bufferedReader(StandardCharsets.UTF_8) - .use { reader -> reader.readText() } - } catch (exception: IOException) { - throw GradleException("Could not read checksum metadata from $source.", exception) - } finally { - if (connection is HttpURLConnection) { - connection.disconnect() - } - } - } - /** * Reads a UTF-8 cache metadata value without following a symbolic link. */ diff --git a/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallationDirectory.kt b/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallationDirectory.kt new file mode 100644 index 0000000..0cd0c88 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallationDirectory.kt @@ -0,0 +1,188 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.embedcode.gradle + +import org.gradle.api.GradleException +import java.io.IOException +import java.nio.file.Files +import java.nio.file.LinkOption +import java.nio.file.Path + +/** + * Owns a normalized installation root and validates paths used by one task action. + * + * The task checks all configured paths before [prepare] creates the root. Later operations reuse + * this instance and recheck the root without repeating its creation. + */ +internal class InstallationDirectory(path: Path) { + + private val root = path.toAbsolutePath().normalize() + + /** + * Normalizes [path] and verifies that it is a strict descendant of the root. + */ + fun requireInside(path: Path): Path { + val normalizedPath = path.toAbsolutePath().normalize() + if (normalizedPath == root || !normalizedPath.startsWith(root)) { + throw GradleException( + "Embed Code installation path `$normalizedPath` must remain inside `$root`.", + ) + } + return normalizedPath + } + + /** + * Creates the installation root and rejects a symlink or non-directory root. + */ + fun prepare() { + try { + if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) { + Files.createDirectories(root) + } + } catch (exception: IOException) { + throw GradleException( + "Could not create Embed Code installation directory `$root`.", + exception, + ) + } + requireRealRoot() + } + + /** + * Rejects a symbolic-link, redirecting, missing, or non-directory root. + */ + private fun requireRealRoot() { + if ( + isRedirectingFileSystemEntry(root) || + !Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS) + ) { + throw GradleException( + "Embed Code installation directory `$root` must be a real directory, " + + "not a symbolic link or redirecting entry.", + ) + } + } + + /** + * Rejects symbolic links in every existing component from the installation root. + */ + fun requireNoSymbolicLinks(path: Path) { + requireRealRoot() + val normalizedPath = requireInside(path) + var current = root + root.relativize(normalizedPath).forEach { component -> + current = current.resolve(component) + if (Files.exists(current, LinkOption.NOFOLLOW_LINKS)) { + if (isRedirectingFileSystemEntry(current)) { + throw GradleException( + "Embed Code installation path `$current` must not be a " + + "symbolic link or redirecting filesystem entry.", + ) + } + if ( + current != normalizedPath && + !Files.isDirectory(current, LinkOption.NOFOLLOW_LINKS) + ) { + throw GradleException( + "Embed Code installation path component `$current` must be a directory.", + ) + } + } + } + } + + /** + * Creates [directory] component by component without following symbolic links. + */ + fun createDirectoriesSafely(directory: Path) { + requireRealRoot() + val normalizedDirectory = directory.toAbsolutePath().normalize() + if (normalizedDirectory != root) { + requireInside(normalizedDirectory) + } + var current = root + root.relativize(normalizedDirectory).forEach { component -> + current = current.resolve(component) + if (Files.exists(current, LinkOption.NOFOLLOW_LINKS)) { + if ( + isRedirectingFileSystemEntry(current) || + !Files.isDirectory(current, LinkOption.NOFOLLOW_LINKS) + ) { + throw GradleException( + "Embed Code installation directory `$current` " + + "must be a real directory, not a symbolic link " + + "or redirecting entry.", + ) + } + } else { + try { + Files.createDirectory(current) + } catch (exception: IOException) { + throw GradleException( + "Could not create Embed Code installation directory `$current`.", + exception, + ) + } + } + } + requireRealPathInside(normalizedDirectory) + } + + /** + * Verifies that [directory] resolves below the real installation root. + */ + private fun requireRealPathInside(directory: Path) { + val realRoot = root.toRealPath() + val realDirectory = directory.toRealPath() + if (!realDirectory.startsWith(realRoot)) { + throw GradleException( + "Embed Code installation directory `$realDirectory` " + + "must remain inside `$realRoot`.", + ) + } + } +} + +/** + * Detects symbolic links and directory redirects such as Windows junctions. + */ +internal fun isRedirectingFileSystemEntry(path: Path): Boolean { + return try { + if (Files.isSymbolicLink(path)) { + true + } else { + val parent = path.parent ?: return false + val expectedRealPath = parent.toRealPath().resolve(path.fileName).normalize() + path.toRealPath() != expectedRealPath + } + } catch (exception: IOException) { + throw GradleException( + "Could not inspect Embed Code installation path `$path`.", + exception, + ) + } +} diff --git a/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/ChecksumSpec.kt b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/ChecksumSpec.kt index 790eacf..45bcf04 100644 --- a/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/ChecksumSpec.kt +++ b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/ChecksumSpec.kt @@ -26,6 +26,8 @@ package io.spine.embedcode.gradle +import com.sun.net.httpserver.HttpExchange +import com.sun.net.httpserver.HttpServer import org.gradle.api.GradleException import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotEquals @@ -33,11 +35,20 @@ import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.net.InetSocketAddress import java.net.URI +import java.nio.file.Files +import java.nio.file.Path +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicReference @DisplayName("Checksum support should") internal class ChecksumSpec { + @TempDir + private lateinit var temporaryDirectory: Path + @Test fun `normalize a prefixed uppercase digest`() { val digest = "A".repeat(64) @@ -56,6 +67,17 @@ internal class ChecksumSpec { assertEquals("Invalid SHA-256 digest `$digest`.", error.message) } + @Test + fun `reject a digest with an invalid length`() { + val digest = "0".repeat(63) + + val error = assertThrows(GradleException::class.java) { + normalizeSha256(digest) + } + + assertEquals("Invalid SHA-256 digest `$digest`.", error.message) + } + @Test fun `distinguish release assets in cache metadata`() { val baseUrl = "https://github.com/SpineEventEngine/embed-code-go/releases" @@ -96,6 +118,16 @@ internal class ChecksumSpec { assertNull(githubReleaseApi("https:///releases", "v1.2.4")) } + @Test + fun `ignore a GitHub URL outside the release path`() { + assertNull( + githubReleaseApi( + "https://github.com/SpineEventEngine/embed-code-go/downloads", + "v1.2.4", + ), + ) + } + @Test fun `read an asset digest from GitHub JSON`() { val digest = "2".repeat(64) @@ -105,6 +137,189 @@ internal class ChecksumSpec { assertEquals(digest, parseGitHubAssetSha256(json, "embed-code-linux")) } + @Test + fun `send GitHub headers when reading checksum metadata`() { + val authorization = AtomicReference() + val accept = AtomicReference() + val userAgent = AtomicReference() + val server = startServer() + server.createContext("/metadata") { exchange -> + authorization.set(exchange.requestHeaders.getFirst("Authorization")) + accept.set(exchange.requestHeaders.getFirst("Accept")) + userAgent.set(exchange.requestHeaders.getFirst("User-Agent")) + exchange.respond(200, """{"assets":[]}""") + } + server.start() + + try { + val result = readChecksumMetadata(server.uri("/metadata"), "secret-token") + + assertEquals("""{"assets":[]}""", result) + assertEquals("Bearer secret-token", authorization.get()) + assertEquals("application/vnd.github+json", accept.get()) + assertEquals("embed-code-gradle-plugin", userAgent.get()) + } finally { + server.stop(0) + } + } + + @Test + fun `not follow checksum metadata redirects`() { + val redirectedRequests = AtomicInteger() + val server = startServer() + server.createContext("/metadata") { exchange -> + exchange.responseHeaders.add("Location", server.uri("/redirected").toString()) + exchange.respond(302) + } + server.createContext("/redirected") { exchange -> + redirectedRequests.incrementAndGet() + exchange.respond(200, """{"assets":[]}""") + } + server.start() + + try { + val source = server.uri("/metadata") + + val error = assertThrows(GradleException::class.java) { + readChecksumMetadata(source, "secret-token") + } + + assertEquals( + "Could not read checksum metadata: HTTP 302 from $source.", + error.message, + ) + assertEquals(0, redirectedRequests.get()) + } finally { + server.stop(0) + } + } + + @Test + fun `reject a failed checksum metadata response`() { + val server = startServer() + server.createContext("/metadata") { exchange -> + exchange.respond(503) + } + server.start() + + try { + val source = server.uri("/metadata") + + val error = assertThrows(GradleException::class.java) { + readChecksumMetadata(source) + } + + assertEquals( + "Could not read checksum metadata: HTTP 503 from $source.", + error.message, + ) + } finally { + server.stop(0) + } + } + + @Test + fun `use a trimmed token for the GitHub API host`() { + val source = URI.create("https://api.github.com/repos/owner/repository/releases/tags/v1") + var receivedToken: String? = null + + val result = readGitHubReleaseMetadata(source, " secret-token ") { uri, token -> + assertEquals(source, uri) + receivedToken = token + "metadata" + } + + assertEquals("metadata", result) + assertEquals("secret-token", receivedToken) + } + + @Test + fun `omit a token outside the GitHub API host`() { + val source = URI.create("https://metadata.example.com/releases/v1") + var receivedToken: String? = "not-called" + + readGitHubReleaseMetadata(source, "secret-token") { _, token -> + receivedToken = token + "metadata" + } + + assertNull(receivedToken) + } + + @Test + fun `omit an absent or blank token for the GitHub API host`() { + val source = URI.create("https://api.github.com/repos/owner/repository/releases/tags/v1") + var requests = 0 + + listOf(null, " ").forEach { configuredToken -> + readGitHubReleaseMetadata(source, configuredToken) { _, receivedToken -> + assertNull(receivedToken) + requests++ + "metadata" + } + } + + assertEquals(2, requests) + } + + @Test + fun `read release metadata through the provided transport`() { + val metadata = temporaryDirectory.resolve("metadata.json") + Files.writeString(metadata, "metadata") + + val result = readGitHubReleaseMetadata( + metadata.toUri(), + "secret-token", + ::readChecksumMetadata, + ) + + assertEquals("metadata", result) + } + + @Test + fun `report a checksum metadata read failure`() { + val source = temporaryDirectory.resolve("missing.json").toUri() + + val error = assertThrows(GradleException::class.java) { + readChecksumMetadata(source) + } + + assertEquals("Could not read checksum metadata from $source.", error.message) + } + + @Test + fun `reject a non-object GitHub release response`() { + val error = assertThrows(GradleException::class.java) { + parseGitHubAssetSha256("[]", "embed-code-linux") + } + + assertEquals("The GitHub release response is not a JSON object.", error.message) + } + + @Test + fun `reject a GitHub release response without assets`() { + val error = assertThrows(GradleException::class.java) { + parseGitHubAssetSha256("{}", "embed-code-linux") + } + + assertEquals("The GitHub release response does not contain assets.", error.message) + } + + @Test + fun `reject a GitHub release response without the requested asset`() { + val error = assertThrows(GradleException::class.java) { + parseGitHubAssetSha256( + """{"assets":[{"name":"embed-code-macos"}]}""", + "embed-code-linux", + ) + } + + assertEquals( + "The GitHub release does not contain asset `embed-code-linux`.", + error.message, + ) + } + @Test fun `resolve an asset digest from GitHub release metadata`() { val digest = "3".repeat(64) @@ -202,4 +417,17 @@ internal class ChecksumSpec { error.message, ) } + + private fun startServer(): HttpServer = + HttpServer.create(InetSocketAddress("127.0.0.1", 0), 0) + + private fun HttpServer.uri(path: String): URI = + URI.create("http://127.0.0.1:${address.port}$path") + + private fun HttpExchange.respond(status: Int, body: String = "") { + val content = body.toByteArray() + sendResponseHeaders(status, content.size.toLong()) + responseBody.use { output -> output.write(content) } + close() + } } diff --git a/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/EmbedCodeJsonSpec.kt b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/EmbedCodeJsonSpec.kt index 5ec1fe4..a705af6 100644 --- a/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/EmbedCodeJsonSpec.kt +++ b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/EmbedCodeJsonSpec.kt @@ -40,7 +40,8 @@ internal class EmbedCodeJsonSpec { "C:\\docs\nline", listOf("**/\"quoted\".md", "line\nbreak"), listOf("drafts\\**"), - "---\n---", + // Kotlin has no `\f` escape, so form feed uses its Unicode code point. + "---\b\u000C\r\t\u0001---", info = true, stacktrace = false, ) @@ -54,7 +55,7 @@ internal class EmbedCodeJsonSpec { "docs-path": "C:\\docs\nline", "doc-includes": ["**/\"quoted\".md", "line\nbreak"], "doc-excludes": ["drafts\\**"], - "separator": "---\n---", + "separator": "---\b\f\r\t\u0001---", "info": true, "stacktrace": false } diff --git a/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/InstallationDirectorySpec.kt b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/InstallationDirectorySpec.kt new file mode 100644 index 0000000..61ca93b --- /dev/null +++ b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/InstallationDirectorySpec.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.embedcode.gradle + +import org.gradle.api.GradleException +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.EnabledOnOs +import org.junit.jupiter.api.condition.OS +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Files +import java.nio.file.Path + +@DisplayName("`InstallationDirectory` should") +internal class InstallationDirectorySpec { + + @TempDir + private lateinit var temporaryDirectory: Path + + @Test + fun `reject the installation root as its own descendant`() { + val root = temporaryDirectory.resolve("installation") + val installation = InstallationDirectory(root) + + val error = assertThrows(GradleException::class.java) { + installation.requireInside(root) + } + + assertEquals( + "Embed Code installation path `$root` must remain inside `$root`.", + error.message, + ) + } + + @Test + fun `report a failure to prepare the installation root`() { + val blockingFile = temporaryDirectory.resolve("blocked") + Files.writeString(blockingFile, "unchanged") + val root = blockingFile.resolve("installation") + val installation = InstallationDirectory(root) + + val error = assertThrows(GradleException::class.java) { + installation.prepare() + } + + assertEquals( + "Could not create Embed Code installation directory `$root`.", + error.message, + ) + assertEquals("unchanged", Files.readString(blockingFile)) + } + + @Test + fun `reject a regular file as the installation root`() { + val root = temporaryDirectory.resolve("installation") + Files.writeString(root, "unchanged") + val installation = InstallationDirectory(root) + + val error = assertThrows(GradleException::class.java) { + installation.prepare() + } + + assertEquals( + "Embed Code installation directory `$root` must be a real directory, " + + "not a symbolic link or redirecting entry.", + error.message, + ) + assertEquals("unchanged", Files.readString(root)) + } + + @Test + fun `create a missing directory and accept the installation root`() { + val root = temporaryDirectory.resolve("installation") + val child = root.resolve("child") + val installation = InstallationDirectory(root) + installation.prepare() + + installation.createDirectoriesSafely(root) + installation.createDirectoriesSafely(child) + + assertTrue(Files.isDirectory(child)) + } + + @Test + fun `reject a regular file while creating a directory`() { + val root = temporaryDirectory.resolve("installation") + val blockingFile = root.resolve("blocked") + val installation = InstallationDirectory(root) + installation.prepare() + Files.writeString(blockingFile, "unchanged") + + val error = assertThrows(GradleException::class.java) { + installation.createDirectoriesSafely(blockingFile.resolve("child")) + } + + assertEquals( + "Embed Code installation directory `$blockingFile` must be a real directory, " + + "not a symbolic link or redirecting entry.", + error.message, + ) + assertEquals("unchanged", Files.readString(blockingFile)) + } + + @Test + @EnabledOnOs(OS.LINUX, OS.MAC) + fun `report a directory creation failure`() { + val root = temporaryDirectory.resolve("installation") + val invalidDirectory = root.resolve("a".repeat(256)) + val installation = InstallationDirectory(root) + installation.prepare() + + val error = assertThrows(GradleException::class.java) { + installation.createDirectoriesSafely(invalidDirectory) + } + + assertEquals( + "Could not create Embed Code installation directory `$invalidDirectory`.", + error.message, + ) + assertFalse(Files.exists(invalidDirectory)) + } + + @Test + fun `report a failure to inspect a filesystem entry`() { + val missing = temporaryDirectory.resolve("missing") + + val error = assertThrows(GradleException::class.java) { + isRedirectingFileSystemEntry(missing) + } + + assertEquals( + "Could not inspect Embed Code installation path `$missing`.", + error.message, + ) + } + + @Test + @EnabledOnOs(OS.LINUX, OS.MAC) + fun `accept the filesystem root as non-redirecting`() { + assertFalse(isRedirectingFileSystemEntry(Path.of("/"))) + } + + @Test + @EnabledOnOs(OS.LINUX, OS.MAC) + fun `reject a symbolic link while creating a directory`() { + val root = temporaryDirectory.resolve("installation") + val outside = temporaryDirectory.resolve("outside") + Files.createDirectories(outside) + val redirect = root.resolve("redirect") + val installation = InstallationDirectory(root) + installation.prepare() + Files.createSymbolicLink(redirect, outside) + + val error = assertThrows(GradleException::class.java) { + installation.createDirectoriesSafely(redirect.resolve("child")) + } + + assertEquals( + "Embed Code installation directory `$redirect` must be a real directory, " + + "not a symbolic link or redirecting entry.", + error.message, + ) + assertFalse(Files.exists(outside.resolve("child"))) + } +} diff --git a/pom.xml b/pom.xml index cfb82b2..ece629c 100644 --- a/pom.xml +++ b/pom.xml @@ -37,6 +37,18 @@ 2.0.0-alpha.5 provided + + org.jacoco + org.jacoco.agent + 0.8.14 + provided + + + org.jacoco + org.jacoco.ant + 0.8.14 + provided + org.jetbrains.dokka all-modules-page-plugin