diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8d0640c7..cb0e74a0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @jonesbusy +* @andreaTP diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 49aa9112..4d809c33 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -1,4 +1,4 @@ -# Copyright The ORAS Authors. +# Copyright The ORAS Authors and roastedroot contributors. # 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 @@ -11,7 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -name: 🐛 Bug report +name: Bug report description: File a bug report labels: ["bug"] body: diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index badc3481..1969d1ba 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -1,4 +1,4 @@ -# Copyright The ORAS Authors. +# Copyright The ORAS Authors and roastedroot contributors. # 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 @@ -18,7 +18,7 @@ body: - type: markdown id: preface attributes: - value: "Thank you for submitting new features for ORAS." + value: "Thank you for submitting new features for the ORAS Java SDK." - type: textarea id: description attributes: @@ -28,7 +28,7 @@ body: - type: textarea id: solution attributes: - label: "Why is this needed for ORAS?" + label: "Why is this needed?" description: "Please describe your user story or scenario." validations: required: true @@ -36,7 +36,6 @@ body: id: idea attributes: label: "Are you willing to submit PRs to contribute to this feature?" - description: "This is absolutely not required, but we are happy to guide you in the contribution process - especially when you already have a good proposal or understanding of how to implement it. Join us at https://slack.cncf.io/ and choose #oras channel." + description: "This is absolutely not required, but we are happy to guide you in the contribution process." options: - label: Yes, I am willing to implement it. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index ed00a7f4..d36f38fe 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -28,5 +28,5 @@ --> ### Submitter checklist -- [ ] I have read and understood the [CONTRIBUTING](https://github.com/oras-project/oras-java/blob/main/CONTRIBUTING.md) guide +- [ ] I have read and understood the [CONTRIBUTING](https://github.com/roastedroot/oras-java/blob/main/CONTRIBUTING.md) guide - [ ] I have run `mvn license:update-file-header`, `mvn spotless:apply`, `pre-commit run -a`, `mvn clean install` before opening the PR diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml deleted file mode 100644 index a7a9afc3..00000000 --- a/.github/release-drafter.yml +++ /dev/null @@ -1,53 +0,0 @@ -# Configuration for Release Drafter: https://github.com/toolmantim/release-drafter ---- - -# Always use 3 digits for the version number -name-template: $NEXT_PATCH_VERSION -tag-template: $NEXT_PATCH_VERSION -version-template: $MAJOR.$MINOR.$PATCH - -categories: - - title: 💥 Breaking changes - labels: - - breaking - - title: ⚠️ Deprecated - labels: - - deprecated - - title: 🚀 New features and improvements - labels: - - enhancement - - feature - - title: 🐛 Bug fixes - labels: - - bug - - fix - - bugfix - - regression - - regression-fix - - title: 📝 Documentation updates - labels: - - documentation - - title: 👻 Maintenance - labels: - - chore - - internal - - maintenance - - title: 🚦 Tests - labels: - - test - - tests - - title: ✍ Other changes - # Default label used by Dependabot - - title: 📦 Dependency updates - labels: - - dependencies - collapse-after: 15 -exclude-labels: - - reverted - - no-changelog - - skip-changelog - - invalid - -template: | - - $CHANGES diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ee780dcb..03b8e5fd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,10 +3,7 @@ name: PR build on: workflow_dispatch: pull_request: - branches: [main, release/0.2.x] - push: - branches: - - release/0.2.x + branches: [main] permissions: contents: read @@ -35,7 +32,7 @@ jobs: needs: build strategy: matrix: - java: [17, 21, 25] + java: [11, 17, 21, 25] steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -59,42 +56,3 @@ jobs: - name: Maven build run: mvn --batch-mode --update-snapshots verify - - - name: Upload coverage reports to Codecov - if: matrix.java == '17' - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - with: - report_type: coverage - token: ${{ secrets.CODECOV_TOKEN }} - verbose: true - - - name: Upload test reports to Codecov - if: matrix.java == '17' - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - with: - report_type: test_results - token: ${{ secrets.CODECOV_TOKEN }} - verbose: true - - maven-build-windows: - runs-on: windows-latest - needs: build - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Read Maven version - id: maven-version - run: echo "version=$(cat .github/vars/maven-version.txt)" >> $env:GITHUB_OUTPUT - - - name: Setup Maven Action - uses: s4u/setup-maven-action@ba34de01b7f4ba2ab8e2860df8993a29f4477056 # v1.20.0 - with: - checkout-enabled: false - java-distribution: 'temurin' - java-version: 17 - maven-version: ${{ steps.maven-version.outputs.version }} - cache-enabled: true - - - name: Maven build - run: mvn --batch-mode --update-snapshots verify diff --git a/.github/workflows/deploy-javadoc.yml b/.github/workflows/deploy-javadoc.yml index 9a62bcf6..587fd5c6 100644 --- a/.github/workflows/deploy-javadoc.yml +++ b/.github/workflows/deploy-javadoc.yml @@ -30,7 +30,7 @@ jobs: with: checkout-enabled: false java-distribution: 'temurin' - java-version: 17 + java-version: 11 maven-version: ${{ steps.maven-version.outputs.version }} cache-enabled: true diff --git a/.github/workflows/deploy-snapshots.yml b/.github/workflows/deploy-snapshots.yml deleted file mode 100644 index f2d48441..00000000 --- a/.github/workflows/deploy-snapshots.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: Deploy Snaphosts - -on: - workflow_dispatch: - push: - branches: - - main - -permissions: - contents: read - -jobs: - deploy-snapshots: - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - packages: write - steps: - - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Read Maven version - id: maven-version - run: echo "version=$(cat .github/vars/maven-version.txt)" >> $GITHUB_OUTPUT - - - name: Setup Maven Action - uses: s4u/setup-maven-action@ba34de01b7f4ba2ab8e2860df8993a29f4477056 # v1.20.0 - with: - checkout-enabled: false - java-distribution: 'temurin' - java-version: 17 - maven-version: ${{ steps.maven-version.outputs.version }} - cache-enabled: true - settings-servers: | - [{ - "id": "central-snapshots", - "username": "${{ secrets.CENTRAL_USERNAME }}", - "password": "${{ secrets.CENTRAL_PASSWORD }}" - }] - - - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 - with: - gpg_private_key: ${{ secrets.GPG_KEY }} - passphrase: ${{ secrets.GPG_PASSPHRASE }} - - name: List keys - run: gpg -K - - - name: Maven build - run: mvn --batch-mode -Psign --update-snapshots -Dmaven.resolver.transport=wagon deploy - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - with: - report_type: coverage - token: ${{ secrets.CODECOV_TOKEN }} - - - name: Upload test reports to Codecov - if: matrix.java == '17' - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - with: - report_type: test_results - token: ${{ secrets.CODECOV_TOKEN }} - verbose: true diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml deleted file mode 100644 index 4ff54ca0..00000000 --- a/.github/workflows/release-drafter.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Release Drafter - -on: - workflow_dispatch: - push: - branches: - - main - -permissions: - contents: read - -jobs: - update_release_draft: - permissions: - contents: write - pull-requests: write - runs-on: ubuntu-latest - steps: - - uses: release-drafter/release-drafter@34d80673e067bdc0c24568d3af899c216adcfaa9 # v7.7.0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f714648c..49e75276 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,86 +1,80 @@ -name: Release +name: release on: workflow_dispatch: + inputs: + release-version: + description: 'Version being released' + required: true + branch: + description: 'Branch to release from' + required: true + default: 'main' permissions: - contents: read + contents: write jobs: - deploy-release: + release: + name: Release runs-on: ubuntu-latest - permissions: - contents: write - id-token: write - packages: write - steps: + steps: - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Read Maven version - id: maven-version - run: echo "version=$(cat .github/vars/maven-version.txt)" >> $GITHUB_OUTPUT + uses: actions/checkout@v7 - - name: Setup Maven Action - uses: s4u/setup-maven-action@ba34de01b7f4ba2ab8e2860df8993a29f4477056 # v1.20.0 + - name: Setup Java + uses: actions/setup-java@v5 with: - checkout-enabled: false - java-distribution: 'temurin' - java-version: 17 - maven-version: ${{ steps.maven-version.outputs.version }} - cache-enabled: true - settings-servers: | - [{ - "id": "central", - "username": "${{ secrets.CENTRAL_USERNAME }}", - "password": "${{ secrets.CENTRAL_PASSWORD }}" - }] + java-version: 11 + distribution: 'temurin' + server-id: central + server-username: MAVEN_USERNAME + server-password: MAVEN_CENTRAL_TOKEN + gpg-private-key: ${{ secrets.java_gpg_secret_key }} + gpg-passphrase: MAVEN_GPG_PASSPHRASE - - name: Configure Git + - id: install-secret-key + name: Install gpg secret key run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git config gpg.format openpgp - git config commit.gpgsign true - git config user.signingkey CB5831CAFA9D8C6AFA9FC9DA67E69853B952BA35 + cat <(echo -e "${{ secrets.java_gpg_secret_key }}") | gpg --batch --import + gpg --list-secret-keys --keyid-format LONG - - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 - with: - gpg_private_key: ${{ secrets.GPG_KEY }} - passphrase: ${{ secrets.GPG_PASSPHRASE }} - - name: List keys - run: gpg -K + - name: Install docker-credential-fake + run: sudo cp docker-credential-fake /usr/bin/docker-credential-fake - - name: Setup SSH - uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0 - with: - ssh-private-key: ${{ secrets.RELEASE_SSH_KEY }} + - name: Compile + run: mvn --batch-mode clean install -DskipTests - - name: Maven release - run: mvn --batch-mode --update-snapshots -Dmaven.resolver.transport=wagon -DskipTests -Darguments="-DskipTests" release:prepare release:perform + - name: Setup Git + run: | + git config --global user.name "GitHub Actions" + git config --global user.email "actions@github.com" + + - name: Set the version + run: | + mvn versions:set -DgenerateBackupPoms=false -DnewVersion=${{ github.event.inputs.release-version }} + git add . + git commit -m "Release version update ${{ github.event.inputs.release-version }}" + git push + git tag ${{ github.event.inputs.release-version }} + git push origin ${{ github.event.inputs.release-version }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Finalize release + - name: Release to Maven Central + run: | + mvn --batch-mode deploy -Prelease -DskipTests=true + env: + MAVEN_USERNAME: ${{ secrets.central_username }} + MAVEN_CENTRAL_TOKEN: ${{ secrets.central_password }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.java_gpg_passphrase }} + + - name: Back to Snapshot run: | - draft_id=$(gh api /repos/oras-project/oras-java/releases | jq -r '.[] | select(.draft == true) | .id') - echo "Draft ID: $draft_id" - gh api -X PATCH -F draft=false /repos/oras-project/oras-java/releases/$draft_id - release_tag_name=$(gh api /repos/oras-project/oras-java/releases/latest | jq -r '.tag_name') - echo "Release tag: $release_tag_name" - for file in target/checkout/target/*.jar target/checkout/target/*.jar.asc target/checkout/target/*.jar.sigstore.json; do - if [[ -f "$file" ]]; then - if [[ "$file" != *javadoc* && "$file" != *sources* && "$file" != *tests* ]]; then - echo "Uploading $file" - gh release upload "$release_tag_name" "$file" --repo oras-project/oras-java - else - echo "Skipping $file" - fi - else - echo "Skipping missing file: $file" - fi - done + mvn versions:set -DgenerateBackupPoms=false -DnewVersion=999-SNAPSHOT + git add . + git commit -m "Snapshot version update" + git push env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml deleted file mode 100644 index 80c81d61..00000000 --- a/.github/workflows/scorecard.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Security ossf scorecard -on: - workflow_dispatch: - push: - branches: [ "main" ] - -permissions: read-all - -jobs: - analysis: - name: Scorecard analysis - runs-on: ubuntu-latest - permissions: - security-events: write - id-token: write - - steps: - - name: Checkout code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Run analysis - uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 - with: - results_file: results.sarif - results_format: sarif - publish_results: true - - - name: Upload artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: SARIF file - path: results.sarif - retention-days: 5 - - - name: Upload to code-scanning - uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v3.29.5 - with: - sarif_file: results.sarif diff --git a/.github/workflows/updatecli.yaml b/.github/workflows/updatecli.yaml deleted file mode 100644 index c1d2dbfe..00000000 --- a/.github/workflows/updatecli.yaml +++ /dev/null @@ -1,37 +0,0 @@ -name: updatecli -on: - workflow_dispatch: - schedule: - - cron: '0 2 * * 1' # Every Monday at 2am UTC - push: - pull_request: -permissions: - contents: read - -jobs: - updatecli: - permissions: - contents: write - pull-requests: write - runs-on: ubuntu-latest - if: github.repository_owner == 'oras-project' - steps: - - - name: Checkout code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Install Updatecli in the runner - uses: updatecli/updatecli-action@41b9c8d707830a9daebaeaa84c1b62d60b779564 # v3.6.0 - - - name: Run Updatecli in Dry Run mode - run: updatecli diff --config ./updatecli/updatecli.d --values ./updatecli/values.github-action.yaml - env: - UPDATECLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Run Updatecli in Apply mode - if: github.ref == 'refs/heads/main' - run: updatecli apply --config ./updatecli/updatecli.d --values ./updatecli/values.github-action.yaml - env: - UPDATECLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.sdkmanrc b/.sdkmanrc index ff53b5e2..7559d988 100644 --- a/.sdkmanrc +++ b/.sdkmanrc @@ -1,2 +1,2 @@ -java=17.0.19-tem +java=11.0.25-tem maven=3.9.16 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 24660b62..67e09e28 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,14 +1,12 @@ # Contributing -Please refer to the [ORAS Contributing guide](https://oras.land/docs/community/contributing_guide) for general aspects. +Thank you for your interest in contributing to the ORAS Java SDK (Java 11 Fork). ## Java SDK -This section is specific to the Java SDK. - ### Requirements -- Java 17 or later +- Java 11 or later - Maven 3.9.9 or later - Container engine like Docker or Podman (due to testcontainers) - Pre-commit `3.6.2` or later diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..5479e408 --- /dev/null +++ b/NOTICE @@ -0,0 +1,8 @@ +ORAS Java SDK (Java 11 Fork) +Copyright 2026 roastedroot contributors + +This product includes software developed by the ORAS project +(https://github.com/oras-project/oras-java) + +Original work: Copyright 2024-2026 The ORAS Authors +Licensed under the Apache License, Version 2.0 diff --git a/README.md b/README.md index 5688bc7e..bacb99b1 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,31 @@ -# ORAS Java +# ORAS Java SDK (Java 11 Fork) -[![GitHub Workflow Status](https://github.com/oras-project/oras-java/actions/workflows/build.yml/badge.svg)](https://github.com/oras-project/oras-java/actions/workflows/build.yml) -[![codecov](https://codecov.io/gh/oras-project/oras-java/branch/main/graph/badge.svg)](https://codecov.io/gh/oras-project/oras-java) -![GitHub Release](https://img.shields.io/github/v/release/oras-project/oras-java?logo=github&color=green) -[![GitHub license](https://img.shields.io/github/license/oras-project/oras-java)](https://github.com/oras-project/oras-java/blob/main/LICENSE) -[![Javadoc](https://img.shields.io/badge/javadoc-latest-blue)](https://oras-project.github.io/oras-java/) -[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/oras-project/oras-java/badge)](https://scorecard.dev/viewer/?uri=github.com/oras-project/oras-java) -[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10047/badge)](https://www.bestpractices.dev/projects/10047) -[![Reproducible Central Artifact](https://img.shields.io/reproducible-central/artifact/land.oras/oras-java-sdk/0.2.15)](https://github.com/jvm-repo-rebuild/reproducible-central/blob/master/content/land/oras/oras-java-sdk/README.md) +[![Build](https://github.com/roastedroot/oras-java/actions/workflows/build.yml/badge.svg)](https://github.com/roastedroot/oras-java/actions/workflows/build.yml) - -> [!WARNING] -> The Oras Java SDK is currently in **alpha** state. +> **Note:** This is a fork of [oras-project/oras-java](https://github.com/oras-project/oras-java) +> maintained with **Java 11** compatibility. The upstream project targets Java 17+. > -> It's configuration and APIs might change in future releases - -

-banner -

+> The Java packages remain `land.oras.*` — you can switch between this fork and +> upstream by changing only the Maven/Gradle coordinates. OCI Registry as Storage enables libraries to push OCI Artifacts to [OCI Conformant](https://github.com/opencontainers/oci-conformance) registries. This is a Java SDK for Java developers to empower them to do this in their applications. ## Consuming SDK -SNAPSHOT for version 0.2.x are published on GitHub Maven packages. -SNAPSHOT for version 0.3.x and above are published on Maven Central at: https://central.sonatype.com/repository/maven-snapshots/ - -Releases are published on Maven Central since version 0.2.x. - -Javadoc is published from main branch into: https://oras-project.github.io/oras-java/ +### Maven ```xml - land.oras - oras-java-sdk + io.roastedroot + oras-java VERSION_HERE ``` -### Quarkus - -Quarkus users can use the extension `quarkus-oras` to use the SDK in their applications. - -Follow the [Quarkus ORAS documentation](https://docs.quarkiverse.io/quarkus-oras/dev/index.html#) to get started with Quarkus. - -### Only for SNAPSHOTS (only for testing) +### Gradle -Then on your `pom.xml` - -```xml - - - central-snapshots - ORAS Maven Central SNAPSHOTS - https://central.sonatype.com/repository/maven-snapshots/ - - true - - - false - - - +```groovy +implementation 'io.roastedroot:oras-java:VERSION_HERE' ``` ## Examples @@ -310,7 +274,7 @@ security decision is evaluated against the **effective (resolved) reference** the reference originally supplied. For each operation the SDK: 1. **Resolves** the reference: short-name / unqualified-search expansion (e.g. - `nginx` → `docker.io/library/nginx`), `prefix` → `location` rewrites, and mirror + `nginx` -> `docker.io/library/nginx`), `prefix` -> `location` rewrites, and mirror selection. 2. **Evaluates** `blocked`, `insecure` (HTTP vs HTTPS) and the trust policy against that resolved reference. @@ -367,10 +331,10 @@ When a policy is set, every manifest/index pull is evaluated against it and reje | Type | Supported | Behaviour | |--------------------------|-----------|--------------------------------------------------------------------| -| `insecureAcceptAnything` | ✅ | Accept the image without any verification (trust all). | -| `reject` | ✅ | Reject the image unconditionally. | -| `sigstoreSigned` | ✅ | Accept only images with a valid keyed Sigstore (cosign) signature. | -| `signedBy` (GPG) | ❌ | Not implemented. Legacy | +| `insecureAcceptAnything` | Yes | Accept the image without any verification (trust all). | +| `reject` | Yes | Reject the image unconditionally. | +| `sigstoreSigned` | Yes | Accept only images with a valid keyed Sigstore (cosign) signature. | +| `signedBy` (GPG) | No | Not implemented. Legacy | ### `sigstoreSigned` @@ -401,19 +365,11 @@ cosign bundle payload carries only the image digest and no claimed Docker refere } ``` -### Deploy SNAPSHOTS - -SNAPSHOTS are automatically deployed when the `main` branch is updated. See the [GitHub Actions](.github/workflows/deploy-snapshots.yml) for more details. - -### Perform release - -- Ensure the draft release version correspond to the version on the `pom.xml`. Specially if changing the major or minor version. Patch releases are automatically updated. -- Run the release workflow - -## Code of Conduct +## Attribution -Please note that this project has adopted the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). -Please follow it in all your interactions with the project members and users. +This project is a fork of [oras-project/oras-java](https://github.com/oras-project/oras-java), +originally created by [Valentin Delaye](https://github.com/jonesbusy) and the ORAS Authors. +We are grateful to the original contributors for their work on the ORAS Java SDK. ## License diff --git a/SECURITY.md b/SECURITY.md index ffefe341..99a19303 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,3 +1,5 @@ # Security Policy -Please follow the [security policy](https://oras.land/docs/community/reporting_security_concerns) to report a security vulnerability or concern. +To report a security vulnerability, please use +[GitHub Security Advisories](https://github.com/roastedroot/oras-java/security/advisories) +for this repository. diff --git a/pom.xml b/pom.xml index f858cf3f..52da3e48 100644 --- a/pom.xml +++ b/pom.xml @@ -2,13 +2,13 @@ 4.0.0 - land.oras - oras-java-sdk - 0.8.3-SNAPSHOT + io.roastedroot + oras-java + 999-SNAPSHOT jar ${project.groupId}:${project.artifactId} - ORAS Java SDK - https://github.com/oras-project/oras-java + ORAS Java SDK - Java 11 compatible fork + https://github.com/roastedroot/oras-java @@ -24,23 +24,21 @@ ORAS https://oras.land/ + + andreaTP + Andrea Peruffo + andrea.peruffo1982@gmail.com + IBM + - scm:git:git@github.com:oras-project/oras-java.git - scm:git:git@github.com:oras-project/oras-java.git + scm:git:git://github.com/roastedroot/oras-java.git + scm:git:ssh://github.com:roastedroot/oras-java.git HEAD - https://github.com/oras-project/oras-java + https://github.com/roastedroot/oras-java - - - central-snapshots - ORAS Maven Central SNAPSHOTS - https://central.sonatype.com/repository/maven-snapshots/ - - - UTF-8 @@ -56,17 +54,17 @@ 2.0.18 1.0.1 - 3.2.1 + 2.18.3 1.28.0 1.5.7-13 1.85 - 3.2.4 - 1.17.0 + 3.1.8 + 1.13.12 1.6.2 - 6.1.3 - 2.0.5 + 5.11.4 + 1.21.4 3.13.2 5.23.0 2.1.8 @@ -88,22 +86,26 @@ 3.5.1 3.12.0 3.4.0 - 3.3.1 3.2.8 0.11.0 - 2.2.0 - 1.0.4 2.7.1 - - 17 - 17 - 17 + + 11 + 11 + 11 + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + io.micrometer micrometer-bom @@ -118,13 +120,6 @@ pom import - - tools.jackson - jackson-bom - ${jackson.version} - pom - import - ch.qos.logback logback-classic @@ -187,12 +182,12 @@ org.testcontainers - testcontainers + junit-jupiter ${testcontainer.version} org.testcontainers - testcontainers-junit-jupiter + testcontainers ${testcontainer.version} @@ -210,6 +205,26 @@ + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.dataformat + jackson-dataformat-toml + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + com.github.ben-manes.caffeine caffeine @@ -238,22 +253,6 @@ org.slf4j slf4j-api - - tools.jackson.core - jackson-core - - - tools.jackson.core - jackson-databind - - - tools.jackson.dataformat - jackson-dataformat-toml - - - tools.jackson.dataformat - jackson-dataformat-yaml - @@ -303,12 +302,12 @@ org.testcontainers - testcontainers + junit-jupiter test org.testcontainers - testcontainers-junit-jupiter + testcontainers test @@ -324,20 +323,6 @@ - - - - false - - - true - - apache-snapshots - apache-snapshots - https://repository.apache.org/content/repositories/snapshots/ - - - @@ -382,19 +367,6 @@ - - org.spdx - spdx-maven-plugin - ${spdx-maven-plugin.version} - - - build-spdx - - createSPDX - - - - org.apache.maven.plugins maven-clean-plugin @@ -532,19 +504,6 @@ - - dev.sigstore - sigstore-maven-plugin - ${sigstore-maven-plugin.version} - - - sign - - sign - - - - org.apache.maven.plugins maven-source-plugin @@ -559,16 +518,6 @@ - - org.apache.maven.plugins - maven-release-plugin - ${maven-release-plugin.version} - - @{project.version} - [ci skip] - release - - org.apache.maven.plugins maven-enforcer-plugin @@ -670,14 +619,6 @@ org.codehaus.mojo license-maven-plugin - - org.spdx - spdx-maven-plugin - - - com.diffplug.spotless - spotless-maven-plugin - org.apache.maven.plugins maven-enforcer-plugin @@ -699,6 +640,20 @@ + + spotless + + [17,) + + + + + com.diffplug.spotless + spotless-maven-plugin + + + + quick-build @@ -706,7 +661,6 @@ true true true - true true true true @@ -714,15 +668,11 @@ release - - true - true - org.apache.maven.plugins - maven-enforcer-plugin + maven-gpg-plugin org.apache.maven.plugins @@ -732,53 +682,10 @@ org.apache.maven.plugins maven-source-plugin - - org.apache.maven.plugins - maven-gpg-plugin - org.sonatype.central central-publishing-maven-plugin - - org.apache.maven.plugins - maven-release-plugin - - - dev.sigstore - sigstore-maven-plugin - - - - - - sign - - - - org.jacoco - jacoco-maven-plugin - - - org.apache.maven.plugins - maven-enforcer-plugin - - - org.apache.maven.plugins - maven-javadoc-plugin - - - org.apache.maven.plugins - maven-source-plugin - - - org.apache.maven.plugins - maven-gpg-plugin - - - dev.sigstore - sigstore-maven-plugin - diff --git a/src/main/java/land/oras/Annotations.java b/src/main/java/land/oras/Annotations.java index ec6c8119..b6133cc3 100644 --- a/src/main/java/land/oras/Annotations.java +++ b/src/main/java/land/oras/Annotations.java @@ -21,32 +21,88 @@ package land.oras; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; import land.oras.utils.JsonUtils; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; /** - * Record for annotations - * - * @param manifestAnnotations Annotations for the manifest - * @param configAnnotations Annotations for the config - * @param filesAnnotations Annotations for the layers/files + * Annotations for an OCI artifact */ @NullMarked @OrasModel -public record Annotations( - Map configAnnotations, - Map manifestAnnotations, - Map> filesAnnotations) { +public final class Annotations { + + private final Map configAnnotations; + private final Map manifestAnnotations; + private final Map> filesAnnotations; + + /** + * Create a new annotations instance + * @param configAnnotations Annotations for the config + * @param manifestAnnotations Annotations for the manifest + * @param filesAnnotations Annotations for the layers/files + */ + public Annotations( + Map configAnnotations, + Map manifestAnnotations, + Map> filesAnnotations) { + this.configAnnotations = configAnnotations; + this.manifestAnnotations = manifestAnnotations; + this.filesAnnotations = filesAnnotations; + } @JsonCreator private Annotations() { this(new HashMap<>(), new HashMap<>(), new HashMap<>()); } + /** + * Get the config annotations + * @return The config annotations + */ + @JsonProperty("configAnnotations") + public Map configAnnotations() { + return configAnnotations; + } + + /** + * Get the manifest annotations + * @return The manifest annotations + */ + @JsonProperty("manifestAnnotations") + public Map manifestAnnotations() { + return manifestAnnotations; + } + + /** + * Get the files annotations + * @return The files annotations + */ + @JsonProperty("filesAnnotations") + public Map> filesAnnotations() { + return filesAnnotations; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Annotations)) return false; + Annotations that = (Annotations) o; + return Objects.equals(configAnnotations, that.configAnnotations) + && Objects.equals(manifestAnnotations, that.manifestAnnotations) + && Objects.equals(filesAnnotations, that.filesAnnotations); + } + + @Override + public int hashCode() { + return Objects.hash(configAnnotations, manifestAnnotations, filesAnnotations); + } + /** * Create a new annotations record with only manifest annotations * @param manifestAnnotations The manifest annotations diff --git a/src/main/java/land/oras/ArtifactType.java b/src/main/java/land/oras/ArtifactType.java index a650d48a..03839f1c 100644 --- a/src/main/java/land/oras/ArtifactType.java +++ b/src/main/java/land/oras/ArtifactType.java @@ -58,7 +58,7 @@ public static ArtifactType from(@Nullable String artifactType) { } // Must match https://datatracker.ietf.org/doc/html/rfc6838 if (!artifactType.matches("^[a-zA-Z0-9!#$%&'*+.^_`{|}~-]+/[a-zA-Z0-9!#$%&'*+.^_`{|}~-]+$")) { - throw new OrasException("Invalid artifact type: %s".formatted(artifactType)); + throw new OrasException(String.format("Invalid artifact type: %s", artifactType)); } return new ArtifactType(artifactType); } diff --git a/src/main/java/land/oras/ContainerRef.java b/src/main/java/land/oras/ContainerRef.java index 325c7044..843b43a6 100644 --- a/src/main/java/land/oras/ContainerRef.java +++ b/src/main/java/land/oras/ContainerRef.java @@ -133,7 +133,7 @@ public boolean isUnqualified() { public String getFullRepository(@Nullable Registry registry) { String namespace = getNamespace(registry); if (namespace != null) { - return "%s/%s".formatted(namespace, repository); + return String.format("%s/%s", namespace, repository); } return repository; } @@ -235,8 +235,8 @@ public ContainerRef withReferrersFallbackTag() { if (digest == null) { throw new OrasException("Digest is required to compute the referrers fallback tag"); } - String fallbackTag = "%s-%s" - .formatted(SupportedAlgorithm.fromDigest(digest).getPrefix(), SupportedAlgorithm.getDigest(digest)); + String fallbackTag = String.format( + "%s-%s", SupportedAlgorithm.fromDigest(digest).getPrefix(), SupportedAlgorithm.getDigest(digest)); return new ContainerRef(registry, unqualified, namespace, repository, fallbackTag, null); } @@ -258,9 +258,9 @@ public SupportedAlgorithm getAlgorithm() { private String getApiPrefix(@Nullable Registry target) { String namespace = getNamespace(target); if (namespace != null) { - return "%s/v2/%s/%s".formatted(getApiRegistry(target), namespace, repository); + return String.format("%s/v2/%s/%s", getApiRegistry(target), namespace, repository); } - return "%s/v2/%s".formatted(getApiRegistry(target), repository); + return String.format("%s/v2/%s", getApiRegistry(target), repository); } /** @@ -269,7 +269,7 @@ private String getApiPrefix(@Nullable Registry target) { * @return The tag URL */ public String getRepositoriesPath(@Nullable Registry target) { - return "%s/v2/_catalog".formatted(getApiRegistry(target)); + return String.format("%s/v2/_catalog", getApiRegistry(target)); } /** @@ -286,7 +286,7 @@ public String getRepositoriesPath() { * @return The tag URL */ public String getTagsPath(@Nullable Registry target) { - return "%s/tags/list".formatted(getApiPrefix(target)); + return String.format("%s/tags/list", getApiPrefix(target)); } /** @@ -350,11 +350,11 @@ public String getBlobsMountPath(@Nullable Registry registry, ContainerRef source if (digest == null) { throw new OrasException("You are required to include a digest"); } - return "%s/blobs/uploads/?mount=%s&from=%s" - .formatted( - getApiPrefix(registry), - digest, - URLEncoder.encode(sourceRef.getFullRepository(registry), StandardCharsets.UTF_8)); + return String.format( + "%s/blobs/uploads/?mount=%s&from=%s", + getApiPrefix(registry), + digest, + URLEncoder.encode(sourceRef.getFullRepository(registry), StandardCharsets.UTF_8)); } /** @@ -365,13 +365,11 @@ public String getBlobsMountPath(@Nullable Registry registry, ContainerRef source */ public String getReferrersPath(@Nullable Registry registry, @Nullable ArtifactType artifactType) { if (artifactType == null) { - return "%s/referrers/%s".formatted(getApiPrefix(registry), digest); + return String.format("%s/referrers/%s", getApiPrefix(registry), digest); } - return "%s/referrers/%s?artifactType=%s" - .formatted( - getApiPrefix(registry), - digest, - URLEncoder.encode(artifactType.toString(), StandardCharsets.UTF_8)); + return String.format( + "%s/referrers/%s?artifactType=%s", + getApiPrefix(registry), digest, URLEncoder.encode(artifactType.toString(), StandardCharsets.UTF_8)); } /** @@ -410,7 +408,7 @@ public String getReferrersPath(@Nullable ArtifactType artifactType) { * @return The manifests URL */ public String getManifestsPath(@Nullable Registry registry) { - return "%s/manifests/%s".formatted(getApiPrefix(registry), digest == null ? tag : digest); + return String.format("%s/manifests/%s", getApiPrefix(registry), digest == null ? tag : digest); } /** @@ -430,7 +428,7 @@ public String getBlobsUploadDigestPath(Registry registry) { if (digest == null) { throw new OrasException("You are required to include a digest"); } - return "%s/blobs/uploads/?digest=%s".formatted(getApiPrefix(registry), digest); + return String.format("%s/blobs/uploads/?digest=%s", getApiPrefix(registry), digest); } /** @@ -439,7 +437,7 @@ public String getBlobsUploadDigestPath(Registry registry) { * @return The blobs upload URL */ public String getBlobsUploadPath(Registry registry) { - return "%s/blobs/uploads/".formatted(getApiPrefix(registry)); + return String.format("%s/blobs/uploads/", getApiPrefix(registry)); } /** @@ -451,7 +449,7 @@ public String getBlobsPath(@Nullable Registry registry) { if (digest == null) { throw new OrasException("You are required to include a digest"); } - return "%s/blobs/%s".formatted(getApiPrefix(registry), digest); + return String.format("%s/blobs/%s", getApiPrefix(registry), digest); } /** @@ -607,7 +605,7 @@ ContainerRef checkBlocked(Registry registry) throws OrasException { // Check registry configuration (blocked registries) if (isBlocked(registry)) { throw new OrasException( - "Access to container reference %s is blocked by registry configuration".formatted(this)); + String.format("Access to container reference %s is blocked by registry configuration", this)); } return this; } @@ -637,7 +635,7 @@ public ContainerRef forRegistry(Registry registry) { String key = registry.getRegistriesConf().getAliasKey(this); if (registry.getRegistry() == null && registry.getRegistriesConf().hasAlias(key)) { String newLocation = registry.getRegistriesConf().getAliases().get(key); - String newRefString = "%s:%s".formatted(newLocation, tag); + String newRefString = String.format("%s:%s", newLocation, tag); LOG.debug("Using {} as an alias to {}", key, newRefString); return ContainerRef.parse(newRefString); } @@ -678,9 +676,9 @@ private String determineFirstUnqualifiedSearchRegistry(Registry registry) { } LOG.debug("Container {} does not exist in unqualified search registry {}", this, searchRegistry); } - throw new OrasException( - "Container reference %s is unqualified and cannot be found in any of the unqualified search registries: %s" - .formatted(this, registry.getRegistriesConf().getUnqualifiedRegistries())); + throw new OrasException(String.format( + "Container reference %s is unqualified and cannot be found in any of the unqualified search registries: %s", + this, registry.getRegistriesConf().getUnqualifiedRegistries())); } @Override @@ -705,14 +703,14 @@ public String toString() { if (isUnqualified()) { if (namespace != null && !namespace.isEmpty()) { - return "%s/%s%s".formatted(namespace, repository, ref); + return String.format("%s/%s%s", namespace, repository, ref); } - return "%s%s".formatted(repository, ref); + return String.format("%s%s", repository, ref); } if (namespace != null && !namespace.isEmpty()) { - return "%s/%s/%s%s".formatted(registry, namespace, repository, ref); + return String.format("%s/%s/%s%s", registry, namespace, repository, ref); } - return "%s/%s%s".formatted(registry, repository, ref); + return String.format("%s/%s%s", registry, repository, ref); } } diff --git a/src/main/java/land/oras/CopyUtils.java b/src/main/java/land/oras/CopyUtils.java index 557557d9..7b34d78b 100644 --- a/src/main/java/land/oras/CopyUtils.java +++ b/src/main/java/land/oras/CopyUtils.java @@ -25,6 +25,7 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; import land.oras.exception.OrasException; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -224,9 +225,9 @@ void copy( if (depth > MAX_COPY_DEPTH) { LOG.error("Depth exceeded for copy of {}", sourceRef.getRepository()); - throw new OrasException( - "Maximum copy recursion depth (%d) exceeded; the source may serve an unbounded index or referrer graph" - .formatted(MAX_COPY_DEPTH)); + throw new OrasException(String.format( + "Maximum copy recursion depth (%d) exceeded; the source may serve an unbounded index or referrer graph", + MAX_COPY_DEPTH)); } boolean includeReferrers = options.includeReferrers(); @@ -310,7 +311,7 @@ else if (source.isIndexMediaType(contentType)) { List filtered = index.getManifests().stream() .filter(d -> options.platformFilter().stream().anyMatch(p -> Platform.matches(d.getPlatform(), p))) - .toList(); + .collect(Collectors.toList()); if (filtered.isEmpty()) { throw new OrasException( "No manifests found in index matching platform filter: " + options.platformFilter()); @@ -373,7 +374,7 @@ else if (source.isIndexMediaType(contentType)) { LOG.debug("Copied index {} with tag {}", pushedIndex, targetTag); } else { - throw new OrasException("Unsupported content type: %s".formatted(contentType)); + throw new OrasException(String.format("Unsupported content type: %s", contentType)); } } diff --git a/src/main/java/land/oras/Descriptor.java b/src/main/java/land/oras/Descriptor.java index 576fd5c3..4fddbb9e 100644 --- a/src/main/java/land/oras/Descriptor.java +++ b/src/main/java/land/oras/Descriptor.java @@ -37,7 +37,7 @@ @OrasModel @JsonPropertyOrder({Const.JSON_PROPERTY_MEDIA_TYPE, Const.JSON_PROPERTY_DIGEST, Const.JSON_PROPERTY_SIZE}) @JsonInclude(JsonInclude.Include.NON_NULL) -public sealed class Descriptor permits Config, Manifest, Layer, Index { +public class Descriptor { /** * The media type of the layer diff --git a/src/main/java/land/oras/Index.java b/src/main/java/land/oras/Index.java index 2bb3c67f..804c45d6 100644 --- a/src/main/java/land/oras/Index.java +++ b/src/main/java/land/oras/Index.java @@ -32,6 +32,7 @@ import java.util.Map; import java.util.Objects; import java.util.function.BiPredicate; +import java.util.stream.Collectors; import land.oras.exception.OrasException; import land.oras.utils.Const; import land.oras.utils.JsonUtils; @@ -163,7 +164,7 @@ public List filter(Platform platform) { public List filter(Platform platform, BiPredicate comparator) { return getManifests().stream() .filter(descriptor -> comparator.test(descriptor.getPlatform(), platform)) - .toList(); + .collect(Collectors.toList()); } /** @@ -173,7 +174,7 @@ public List filter(Platform platform, BiPredicate unspecifiedPlatforms() { return getManifests().stream() .filter(descriptor -> Platform.unspecified(descriptor.getPlatform())) - .toList(); + .collect(Collectors.toList()); } /** diff --git a/src/main/java/land/oras/LayoutRef.java b/src/main/java/land/oras/LayoutRef.java index e5389965..dc509170 100644 --- a/src/main/java/land/oras/LayoutRef.java +++ b/src/main/java/land/oras/LayoutRef.java @@ -198,9 +198,9 @@ public int hashCode() { public String toString() { if (tag != null) { if (isValidDigest()) { - return "%s@%s".formatted(getFolder().toString(), tag); + return String.format("%s@%s", getFolder().toString(), tag); } - return "%s:%s".formatted(getFolder().toString(), tag); + return String.format("%s:%s", getFolder().toString(), tag); } else { return getFolder().toString(); } diff --git a/src/main/java/land/oras/OCI.java b/src/main/java/land/oras/OCI.java index 25312bd7..00c2ba77 100644 --- a/src/main/java/land/oras/OCI.java +++ b/src/main/java/land/oras/OCI.java @@ -20,6 +20,7 @@ package land.oras; +import com.fasterxml.jackson.annotation.JsonAutoDetect; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -36,6 +37,7 @@ import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutorService; import java.util.function.Supplier; +import java.util.stream.Collectors; import land.oras.exception.OrasException; import land.oras.utils.ArchiveUtils; import land.oras.utils.Const; @@ -51,7 +53,12 @@ * Commons methods for OCI operations * @param The reference type */ -public abstract sealed class OCI> permits Registry, OCILayout { +@JsonAutoDetect( + getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE, + setterVisibility = JsonAutoDetect.Visibility.NONE, + fieldVisibility = JsonAutoDetect.Visibility.NONE) +public abstract class OCI> { /** * The logger @@ -342,7 +349,7 @@ protected final List pushLayers( .map(p -> CompletableFuture.supplyAsync( () -> pushLayer(ref, annotations, withDigest, p, options), getExecutorService())) .map(CompletableFuture::join) - .toList(); + .collect(Collectors.toList()); } catch (CompletionException e) { throw new OrasException("Failed to push layers", e.getCause()); } @@ -650,7 +657,7 @@ protected Layer pushLayer(T ref, Annotations annotations, boolean withDigest, Lo // We store the filename, based on directory name if we don't auto unpack if (!autoUnpack) { - title = "%s.%s".formatted(title, compression.getFileExtension()); + title = String.format("%s.%s", title, compression.getFileExtension()); } LOG.debug("Uploading directory as archive with title: {}", title); diff --git a/src/main/java/land/oras/OCILayout.java b/src/main/java/land/oras/OCILayout.java index df315759..124eb056 100644 --- a/src/main/java/land/oras/OCILayout.java +++ b/src/main/java/land/oras/OCILayout.java @@ -36,6 +36,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Supplier; +import java.util.stream.Collectors; import land.oras.OCI.PullOptions; import land.oras.OCI.PushOptions; import land.oras.exception.OrasException; @@ -103,7 +104,7 @@ public boolean mountBlob(LayoutRef sourceRef, LayoutRef targetRef) { .resolve(algorithm.getPrefix()) .resolve(SupportedAlgorithm.getDigest(digest)); if (!Files.exists(sourceBlobPath)) { - throw new OrasException("Source blob not found at: %s".formatted(sourceBlobPath)); + throw new OrasException(String.format("Source blob not found at: %s", sourceBlobPath)); } try { Files.copy(sourceBlobPath, targetBlobPath); @@ -187,8 +188,9 @@ public void pullArtifact(LayoutRef ref, Path path, PullOptions options) { Path targetPath = path.resolve(layer.getAnnotations().get(Const.ANNOTATION_TITLE)) .normalize(); if (!targetPath.startsWith(path.normalize())) { - throw new OrasException("Refusing to pull layer: title annotation is not withing folder '%s'" - .formatted(layer.getAnnotations().get(Const.ANNOTATION_TITLE))); + throw new OrasException(String.format( + "Refusing to pull layer: title annotation is not withing folder '%s'", + layer.getAnnotations().get(Const.ANNOTATION_TITLE))); } if (options.isOverwrite()) { Files.copy(blobPath, targetPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING); @@ -291,7 +293,7 @@ public Manifest getManifest(LayoutRef ref) { ManifestDescriptor descriptor = findManifestDescriptor(ref); Path manifestPath = getBlobPath(descriptor); if (!Files.exists(manifestPath)) { - throw new OrasException("Blob not found: %s".formatted(manifestPath)); + throw new OrasException(String.format("Blob not found: %s", manifestPath)); } return Manifest.fromPath(manifestPath).withDescriptor(descriptor); @@ -336,16 +338,17 @@ void verifyBlobDigest(Path blobPath) { Path fileName = blobPath.getFileName(); Path parent = blobPath.getParent(); if (fileName == null || parent == null || parent.getFileName() == null) { - throw new OrasException("Cannot resolve expected digest for blob path: %s".formatted(blobPath)); + throw new OrasException(String.format("Cannot resolve expected digest for blob path: %s", blobPath)); } - String expectedDigest = "%s:%s".formatted(parent.getFileName(), fileName); + String expectedDigest = String.format("%s:%s", parent.getFileName(), fileName); if (!SupportedAlgorithm.isSupported(expectedDigest)) { - throw new OrasException("Blob is not stored at a content-addressed path: %s".formatted(blobPath)); + throw new OrasException(String.format("Blob is not stored at a content-addressed path: %s", blobPath)); } String actualDigest = SupportedAlgorithm.fromDigest(expectedDigest).digest(blobPath); if (!expectedDigest.equals(actualDigest)) { - throw new OrasException("Blob integrity check failed for %s: expected %s but on-disk content hashes to %s" - .formatted(blobPath, expectedDigest, actualDigest)); + throw new OrasException(String.format( + "Blob integrity check failed for %s: expected %s but on-disk content hashes to %s", + blobPath, expectedDigest, actualDigest)); } } @@ -369,7 +372,7 @@ public Layer pushBlob(LayoutRef ref, Path blob, Map annotations) throw new OrasException("Missing ref"); } if (!SupportedAlgorithm.isSupported(ref.getTag())) { - throw new OrasException("Unsupported digest: %s".formatted(ref.getTag())); + throw new OrasException(String.format("Unsupported digest: %s", ref.getTag())); } String digest = ref.getTag(); ensureAlgorithmPath(digest); @@ -399,7 +402,7 @@ public Layer pushBlob(LayoutRef ref, long size, Supplier stream, Ma } boolean isDigest = SupportedAlgorithm.isSupported(digest); if (!isDigest) { - throw new OrasException("Unsupported digest: %s".formatted(digest)); + throw new OrasException(String.format("Unsupported digest: %s", digest)); } ensureAlgorithmPath(digest); try { @@ -427,11 +430,11 @@ public Layer pushBlob(LayoutRef ref, byte[] data) { throw new OrasException("Missing ref"); } if (!SupportedAlgorithm.isSupported(ref.getTag())) { - throw new OrasException("Unsupported digest: %s".formatted(ref.getTag())); + throw new OrasException(String.format("Unsupported digest: %s", ref.getTag())); } String digest = ref.getAlgorithm().digest(data); if (!ref.getTag().equals(digest)) { - throw new OrasException("Digest mismatch: %s != %s".formatted(ref.getTag(), digest)); + throw new OrasException(String.format("Digest mismatch: %s != %s", ref.getTag(), digest)); } ensureAlgorithmPath(digest); Path blobPath = getBlobAlgorithmPath(digest).resolve(SupportedAlgorithm.getDigest(digest)); @@ -456,7 +459,7 @@ public Tags getTags(LayoutRef ref) { .filter(m -> m.getAnnotations() != null && m.getAnnotations().containsKey(Const.ANNOTATION_REF)) .map(m -> m.getAnnotations().get(Const.ANNOTATION_REF)) .sorted() - .toList(); + .collect(Collectors.toList()); return new Tags(name, tags); } @@ -469,10 +472,10 @@ public Tags getTags(LayoutRef ref, int n, @Nullable String last) { if (last != null) { int lastIndex = tags.indexOf(last); if (lastIndex == -1) { - throw new OrasException("Last tag not found: %s".formatted(last)); + throw new OrasException(String.format("Last tag not found: %s", last)); } } - return new Tags(name, tags.stream().skip(startIndex).limit(n).toList()); + return new Tags(name, tags.stream().skip(startIndex).limit(n).collect(Collectors.toList())); } @Override @@ -732,7 +735,7 @@ private ManifestDescriptor findManifestDescriptor(LayoutRef ref) { && tag.equals(m.getAnnotations().get(Const.ANNOTATION_REF)) || tag.equals(m.getDigest()))) .findFirst() - .orElseThrow(() -> new OrasException("Tag or digest not found: %s".formatted(tag))); + .orElseThrow(() -> new OrasException(String.format("Tag or digest not found: %s", tag))); } private Path getBlobPath(ManifestDescriptor manifestDescriptor) { @@ -823,12 +826,12 @@ private void ensureDigest(LayoutRef ref, Path path) { throw new OrasException("Missing ref"); } if (!SupportedAlgorithm.isSupported(ref.getTag())) { - throw new OrasException("Unsupported digest: %s".formatted(ref.getTag())); + throw new OrasException(String.format("Unsupported digest: %s", ref.getTag())); } SupportedAlgorithm algorithm = SupportedAlgorithm.fromDigest(ref.getTag()); String pathDigest = algorithm.digest(path); if (!ref.getTag().equals(pathDigest)) { - throw new OrasException("Digest mismatch: %s != %s".formatted(ref.getTag(), pathDigest)); + throw new OrasException(String.format("Digest mismatch: %s != %s", ref.getTag(), pathDigest)); } } diff --git a/src/main/java/land/oras/Platform.java b/src/main/java/land/oras/Platform.java index ca9a79b4..be9495b9 100644 --- a/src/main/java/land/oras/Platform.java +++ b/src/main/java/land/oras/Platform.java @@ -20,6 +20,7 @@ package land.oras; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; @@ -30,13 +31,7 @@ import org.jspecify.annotations.Nullable; /** - * Record for platform information - * @param os The operating system of the platform - * @param architecture The architecture of the platform - * @param variant The variant of the platform, which is optional and may be null - * @param osVersion The operating system version of the platform, which is optional and may be - * @param features The features of the platform, which is optional and may be null - * @param osFeatures The operating system features of the platform, which is optional and may be + * Platform information for an OCI manifest */ @NullMarked @OrasModel @@ -48,13 +43,50 @@ Const.PLATFORM_OS_FEATURES }) @JsonInclude(JsonInclude.Include.NON_NULL) -public record Platform( - @Nullable @JsonProperty(Const.PLATFORM_OS) String os, - @Nullable @JsonProperty(Const.PLATFORM_ARCHITECTURE) String architecture, - @Nullable @JsonProperty(Const.PLATFORM_OS_VERSION) String osVersion, - @Nullable @JsonProperty(Const.PLATFORM_VARIANT) String variant, - @Nullable @JsonProperty(Const.PLATFORM_FEATURES) List features, - @Nullable @JsonProperty(Const.PLATFORM_OS_FEATURES) List osFeatures) { +public final class Platform { + + @Nullable + private final String os; + + @Nullable + private final String architecture; + + @Nullable + private final String osVersion; + + @Nullable + private final String variant; + + @Nullable + private final List features; + + @Nullable + private final List osFeatures; + + /** + * Create a new platform + * @param os The operating system + * @param architecture The architecture + * @param osVersion The os version + * @param variant The variant + * @param features The features + * @param osFeatures The os features + */ + @JsonCreator + public Platform( + @Nullable @JsonProperty(Const.PLATFORM_OS) String os, + @Nullable @JsonProperty(Const.PLATFORM_ARCHITECTURE) String architecture, + @Nullable @JsonProperty(Const.PLATFORM_OS_VERSION) String osVersion, + @Nullable @JsonProperty(Const.PLATFORM_VARIANT) String variant, + @Nullable @JsonProperty(Const.PLATFORM_FEATURES) List features, + @Nullable @JsonProperty(Const.PLATFORM_OS_FEATURES) List osFeatures) { + this.os = os; + this.architecture = architecture; + this.osVersion = osVersion; + this.variant = variant; + this.features = features; + this.osFeatures = osFeatures; + } /** * Create a new platform linux/amd64 @@ -169,7 +201,7 @@ public static Platform of(String os, String architecture, @Nullable String varia * Return the os of the platform, or "unknown" if the os is null * @return The os of the platform */ - @Override + @JsonProperty(Const.PLATFORM_OS) public String os() { return os != null ? os : Const.PLATFORM_UNKNOWN; } @@ -178,11 +210,51 @@ public String os() { * Return the os of the platform, or "unknown" if the os is null * @return The os of the platform */ - @Override + @JsonProperty(Const.PLATFORM_ARCHITECTURE) public String architecture() { return architecture != null ? architecture : Const.PLATFORM_UNKNOWN; } + /** + * Return the os version of the platform + * @return The os version + */ + @Nullable + @JsonProperty(Const.PLATFORM_OS_VERSION) + public String osVersion() { + return osVersion; + } + + /** + * Return the variant of the platform + * @return The variant + */ + @Nullable + @JsonProperty(Const.PLATFORM_VARIANT) + public String variant() { + return variant; + } + + /** + * Return the features of the platform + * @return The features + */ + @Nullable + @JsonProperty(Const.PLATFORM_FEATURES) + public List features() { + return features; + } + + /** + * Return the os features of the platform + * @return The os features + */ + @Nullable + @JsonProperty(Const.PLATFORM_OS_FEATURES) + public List osFeatures() { + return osFeatures; + } + /** * Create a new platform with the given features * @param features The features of the platform @@ -257,4 +329,28 @@ public static boolean matches(Platform platform, Platform target, boolean includ } return true; } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (!(o instanceof Platform)) return false; + Platform platform = (Platform) o; + return Objects.equals(os, platform.os) + && Objects.equals(architecture, platform.architecture) + && Objects.equals(osVersion, platform.osVersion) + && Objects.equals(variant, platform.variant) + && Objects.equals(features, platform.features) + && Objects.equals(osFeatures, platform.osFeatures); + } + + @Override + public int hashCode() { + return Objects.hash(os, architecture, osVersion, variant, features, osFeatures); + } + + @Override + public String toString() { + return "Platform[os=" + os + ", architecture=" + architecture + ", osVersion=" + osVersion + ", variant=" + + variant + ", features=" + features + ", osFeatures=" + osFeatures + "]"; + } } diff --git a/src/main/java/land/oras/Ref.java b/src/main/java/land/oras/Ref.java index 19755084..2d8f853e 100644 --- a/src/main/java/land/oras/Ref.java +++ b/src/main/java/land/oras/Ref.java @@ -29,7 +29,7 @@ * @param The type of the ref */ @NullMarked -public abstract sealed class Ref> permits ContainerRef, LayoutRef { +public abstract class Ref> { /** * The tag of the container. diff --git a/src/main/java/land/oras/Registry.java b/src/main/java/land/oras/Registry.java index 376b617c..1404e7e7 100644 --- a/src/main/java/land/oras/Registry.java +++ b/src/main/java/land/oras/Registry.java @@ -35,7 +35,6 @@ import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.HashMap; -import java.util.HexFormat; import java.util.List; import java.util.Map; import java.util.Objects; @@ -45,6 +44,7 @@ import java.util.concurrent.Executors; import java.util.function.BiFunction; import java.util.function.Supplier; +import java.util.stream.Collectors; import land.oras.auth.AuthProvider; import land.oras.auth.AuthStoreAuthenticationProvider; import land.oras.auth.BearerTokenProvider; @@ -175,10 +175,11 @@ private Registry() { @Override public boolean canMount(OCI other, ContainerRef sourceRef, ContainerRef targetRef) { - if (!(other instanceof Registry otherRegistry)) { + if (!(other instanceof Registry)) { LOG.debug("Other OCI is not a registry, cannot mount"); return false; } + Registry otherRegistry = (Registry) other; // Not the same registry String effectiveSourceRegistry = sourceRef.getEffectiveRegistry(this); String effectiveTargetRegistry = targetRef.getEffectiveRegistry(otherRegistry); @@ -218,7 +219,7 @@ public boolean mountBlob(ContainerRef sourceRef, ContainerRef targetRef) { if (!ref.isInsecure(this) && this.isInsecure()) { return copyForNewTransport(ref.getRegistry(), false).mountBlob(sourceRef, ref); } - URI uri = URI.create("%s://%s".formatted(getScheme(), ref.getBlobsMountPath(this, sourceRef))); + URI uri = URI.create(String.format("%s://%s", getScheme(), ref.getBlobsMountPath(this, sourceRef))); HttpClient.ResponseWrapper response = client.post( uri, new byte[0], @@ -407,7 +408,7 @@ private Registry build() { if (executorService == null) { executorService = Executors.newFixedThreadPool(maxConcurrentDownloads, r -> { Thread t = new Thread(r); - t.setName("layer-transfer-worker-%d".formatted(t.getId())); + t.setName(String.format("layer-transfer-worker-%d", t.getId())); return t; }); } @@ -530,7 +531,7 @@ public Tags getTags(ContainerRef containerRef) { if (!ref.isInsecure(this) && this.isInsecure()) { return copyForNewTransport(ref.getRegistry(), false).getTags(ref); } - URI uri = URI.create("%s://%s".formatted(getScheme(), ref.getTagsPath(this))); + URI uri = URI.create(String.format("%s://%s", getScheme(), ref.getTagsPath(this))); HttpClient.ResponseWrapper response = client.get( uri, Map.of(Const.ACCEPT_HEADER, Const.DEFAULT_JSON_MEDIA_TYPE), Scopes.of(ref), authProvider); logResponse(response); @@ -547,9 +548,9 @@ public Tags getTags(ContainerRef containerRef) { Integer n = page.n(); for (int pageNum = 1; last != null; pageNum++) { if (tagListMaxPages > 0 && pageNum >= tagListMaxPages) { - throw new OrasException( - "Tag listing exceeded %d pages: possible self-referential Link header from a rogue registry" - .formatted(tagListMaxPages)); + throw new OrasException(String.format( + "Tag listing exceeded %d pages: possible self-referential Link header from a rogue registry", + tagListMaxPages)); } Tags nextPage = getTags(ref, n == null ? Integer.MAX_VALUE : n, last); @@ -568,7 +569,7 @@ public Tags getTags(ContainerRef containerRef, int n, @Nullable String last) { if (!ref.isInsecure(this) && this.isInsecure()) { return copyForNewTransport(ref.getRegistry(), false).getTags(ref); } - URI uri = URI.create("%s://%s".formatted(getScheme(), ref.getTagsPath(this, n, last))); + URI uri = URI.create(String.format("%s://%s", getScheme(), ref.getTagsPath(this, n, last))); HttpClient.ResponseWrapper response = client.get( uri, Map.of(Const.ACCEPT_HEADER, Const.DEFAULT_JSON_MEDIA_TYPE), Scopes.of(ref), authProvider); logResponse(response); @@ -593,7 +594,7 @@ && getRegistriesConf() return asSecure().getRepositories(); } ContainerRef ref = ContainerRef.parse("default").forRegistry(this); - URI uri = URI.create("%s://%s".formatted(getScheme(), ref.getRepositoriesPath(this))); + URI uri = URI.create(String.format("%s://%s", getScheme(), ref.getRepositoriesPath(this))); HttpClient.ResponseWrapper response = client.get( uri, Map.of(Const.ACCEPT_HEADER, Const.DEFAULT_JSON_MEDIA_TYPE), Scopes.of(ref), authProvider); logResponse(response); @@ -613,7 +614,7 @@ public Referrers getReferrers(ContainerRef containerRef, @Nullable ArtifactType if (!ref.isInsecure(this) && this.isInsecure()) { return copyForNewTransport(ref.getRegistry(), false).getReferrers(ref, artifactType); } - URI uri = URI.create("%s://%s".formatted(getScheme(), ref.getReferrersPath(this, artifactType))); + URI uri = URI.create(String.format("%s://%s", getScheme(), ref.getReferrersPath(this, artifactType))); HttpClient.ResponseWrapper response = client.get( uri, Map.of(Const.ACCEPT_HEADER, Const.DEFAULT_INDEX_MEDIA_TYPE), Scopes.of(ref), authProvider); logResponse(response); @@ -630,11 +631,12 @@ public Referrers getReferrers(ContainerRef containerRef, @Nullable ArtifactType List allManifests = new ArrayList<>(page.getManifests()); for (int pageNum = 1; last != null; pageNum++) { if (referrerListMaxPages > 0 && pageNum >= referrerListMaxPages) { - throw new OrasException( - "Referrer listing exceeded %d pages: possible self-referential Link header from a rogue registry" - .formatted(referrerListMaxPages)); + throw new OrasException(String.format( + "Referrer listing exceeded %d pages: possible self-referential Link header from a rogue registry", + referrerListMaxPages)); } - URI nextUri = URI.create("%s://%s".formatted(getScheme(), ref.getReferrersPath(this, artifactType, last))); + URI nextUri = + URI.create(String.format("%s://%s", getScheme(), ref.getReferrersPath(this, artifactType, last))); HttpClient.ResponseWrapper nextResponse = client.get( nextUri, Map.of(Const.ACCEPT_HEADER, Const.DEFAULT_INDEX_MEDIA_TYPE), Scopes.of(ref), authProvider); logResponse(nextResponse); @@ -655,7 +657,7 @@ public Referrers getReferrers(ContainerRef containerRef, @Nullable ArtifactType */ private Referrers getReferrersFallback(ContainerRef ref, @Nullable ArtifactType artifactType) { ContainerRef fallbackRef = ref.withReferrersFallbackTag(); - URI uri = URI.create("%s://%s".formatted(getScheme(), fallbackRef.getManifestsPath(this))); + URI uri = URI.create(String.format("%s://%s", getScheme(), fallbackRef.getManifestsPath(this))); HttpClient.ResponseWrapper response = client.get( uri, Map.of(Const.ACCEPT_HEADER, Const.DEFAULT_INDEX_MEDIA_TYPE), Scopes.of(fallbackRef), authProvider); logResponse(response); @@ -672,7 +674,7 @@ private Referrers getReferrersFallback(ContainerRef ref, @Nullable ArtifactType if (artifactType != null) { manifests = manifests.stream() .filter(manifest -> artifactType.getMediaType().equals(manifest.getArtifactType())) - .toList(); + .collect(Collectors.toList()); } return Referrers.from(manifests); } @@ -691,7 +693,7 @@ public void deleteManifest(ContainerRef containerRef) { asSecure().deleteManifest(containerRef); return; } - URI uri = URI.create("%s://%s".formatted(getScheme(), ref.getManifestsPath(this))); + URI uri = URI.create(String.format("%s://%s", getScheme(), ref.getManifestsPath(this))); HttpClient.ResponseWrapper response = client.delete(uri, Map.of(), Scopes.of(ref), authProvider); logResponse(response); handleError(response); @@ -717,7 +719,7 @@ public Manifest pushManifest(ContainerRef containerRef, Manifest manifest) { if (!ref.isInsecure(this) && this.isInsecure()) { return copyForNewTransport(ref.getRegistry(), false).pushManifest(ref, manifest); } - URI uri = URI.create("%s://%s".formatted(getScheme(), ref.getManifestsPath(this))); + URI uri = URI.create(String.format("%s://%s", getScheme(), ref.getManifestsPath(this))); byte[] manifestData = manifest.getJson() != null ? manifest.getJson().getBytes() : manifest.toJson().getBytes(); @@ -761,7 +763,7 @@ public Index pushIndex(ContainerRef containerRef, Index index) { if (!ref.isInsecure(this) && this.isInsecure()) { return copyForNewTransport(ref.getRegistry(), false).pushIndex(ref, index); } - URI uri = URI.create("%s://%s".formatted(getScheme(), ref.getManifestsPath(this))); + URI uri = URI.create(String.format("%s://%s", getScheme(), ref.getManifestsPath(this))); byte[] indexData = JsonUtils.toJson(index).getBytes(); LOG.debug("Index data to push: {}", new String(indexData, StandardCharsets.UTF_8)); HttpClient.ResponseWrapper response = client.put( @@ -789,7 +791,7 @@ public void deleteBlob(ContainerRef containerRef) { asSecure().deleteBlob(containerRef); return; } - URI uri = URI.create("%s://%s".formatted(getScheme(), ref.getBlobsPath(this))); + URI uri = URI.create(String.format("%s://%s", getScheme(), ref.getBlobsPath(this))); HttpClient.ResponseWrapper response = client.delete(uri, Map.of(), Scopes.of(ref), authProvider); logResponse(response); handleError(response); @@ -900,7 +902,7 @@ public Layer pushBlob(ContainerRef containerRef, Path blob, Map return Layer.fromFile(blob, ref.getAlgorithm()).withAnnotations(annotations); } URI uri = URI.create( - "%s://%s".formatted(getScheme(), ref.withDigest(digest).getBlobsUploadDigestPath(this))); + String.format("%s://%s", getScheme(), ref.withDigest(digest).getBlobsUploadDigestPath(this))); HttpClient.ResponseWrapper response = client.upload( "POST", uri, @@ -920,8 +922,8 @@ public Layer pushBlob(ContainerRef containerRef, Path blob, Map String location = response.headers().get(Const.LOCATION_HEADER.toLowerCase()); // Ensure location is absolute URI if (!location.startsWith("http") && !location.startsWith("https")) { - location = - "%s://%s/%s".formatted(getScheme(), ref.getApiRegistry(this), location.replaceFirst("^/", "")); + location = String.format( + "%s://%s/%s", getScheme(), ref.getApiRegistry(this), location.replaceFirst("^/", "")); } LOG.debug("Location header: {}", location); @@ -937,7 +939,7 @@ public Layer pushBlob(ContainerRef containerRef, Path blob, Map if (response.statusCode() == 201) { LOG.debug("Successful push: {}", response.response()); } else { - throw new OrasException("Failed to push layer: %s".formatted(response.response())); + throw new OrasException(String.format("Failed to push layer: %s", response.response())); } } @@ -963,7 +965,7 @@ public Layer pushBlob(ContainerRef ref, long size, Supplier stream, return Layer.fromDigest(digest, size).withAnnotations(annotations); } // Empty post without digest - URI uri = URI.create("%s://%s".formatted(getScheme(), containerRef.getBlobsUploadPath(this))); + URI uri = URI.create(String.format("%s://%s", getScheme(), containerRef.getBlobsUploadPath(this))); HttpClient.ResponseWrapper response = client.post( uri, new byte[0], @@ -972,12 +974,13 @@ public Layer pushBlob(ContainerRef ref, long size, Supplier stream, authProvider); logResponse(response); if (response.statusCode() != 202) { - throw new OrasException("Failed to initiate blob upload: %s".formatted(response.response())); + throw new OrasException(String.format("Failed to initiate blob upload: %s", response.response())); } String location = response.headers().get(Const.LOCATION_HEADER.toLowerCase()); // Ensure location is absolute URI if (!location.startsWith("http") && !location.startsWith("https")) { - location = "%s://%s/%s".formatted(getScheme(), ref.getApiRegistry(this), location.replaceFirst("^/", "")); + location = + String.format("%s://%s/%s", getScheme(), ref.getApiRegistry(this), location.replaceFirst("^/", "")); } LOG.debug("Location header: {}", location); @@ -994,7 +997,7 @@ public Layer pushBlob(ContainerRef ref, long size, Supplier stream, if (response.statusCode() == 201) { LOG.debug("Successful push: {}", response.response()); } else { - throw new OrasException("Failed to push layer: %s".formatted(response.response())); + throw new OrasException(String.format("Failed to push layer: %s", response.response())); } handleError(response); return Layer.fromDigest(digest, size).withAnnotations(annotations); @@ -1018,7 +1021,7 @@ public Layer pushBlob(ContainerRef containerRef, byte[] data) { return Layer.fromData(ref, data); } URI uri = URI.create( - "%s://%s".formatted(getScheme(), ref.withDigest(digest).getBlobsUploadDigestPath(this))); + String.format("%s://%s", getScheme(), ref.withDigest(digest).getBlobsUploadDigestPath(this))); HttpClient.ResponseWrapper response = client.post( uri, data, @@ -1037,8 +1040,8 @@ public Layer pushBlob(ContainerRef containerRef, byte[] data) { String location = response.headers().get(Const.LOCATION_HEADER.toLowerCase()); // Ensure location is absolute URI if (!location.startsWith("http") && !location.startsWith("https")) { - location = - "%s://%s/%s".formatted(getScheme(), ref.getApiRegistry(this), location.replaceFirst("^/", "")); + location = String.format( + "%s://%s/%s", getScheme(), ref.getApiRegistry(this), location.replaceFirst("^/", "")); } URI uploadURI = createLocationWithDigest(location, digest); @@ -1053,7 +1056,7 @@ public Layer pushBlob(ContainerRef containerRef, byte[] data) { if (response.statusCode() == 201) { LOG.debug("Successful push: {}", response.response()); } else { - throw new OrasException("Failed to push layer: %s".formatted(response.response())); + throw new OrasException(String.format("Failed to push layer: %s", response.response())); } } @@ -1091,7 +1094,7 @@ public Layer pushBlobChunked(ContainerRef containerRef, Path blob, long chunkSiz long totalSize = Files.size(blob); location = uploadChunks(ref, is, totalSize, chunkSize, location); } catch (IOException e) { - throw new OrasException("Failed to read blob for chunked upload: %s".formatted(blob), e); + throw new OrasException(String.format("Failed to read blob for chunked upload: %s", blob), e); } finalizeChunkedUpload(ref, location, digest); return Layer.fromFile(blob, ref.getAlgorithm()); @@ -1133,7 +1136,7 @@ public Layer pushBlobChunked(ContainerRef containerRef, InputStream stream, long } private String initiateChunkedUpload(ContainerRef ref) { - URI uri = URI.create("%s://%s".formatted(getScheme(), ref.getBlobsUploadPath(this))); + URI uri = URI.create(String.format("%s://%s", getScheme(), ref.getBlobsUploadPath(this))); HttpClient.ResponseWrapper response = client.post( uri, new byte[0], @@ -1143,11 +1146,12 @@ private String initiateChunkedUpload(ContainerRef ref) { logResponse(response); if (response.statusCode() != 202) { throw new OrasException( - "Failed to initiate chunked blob upload: status %d".formatted(response.statusCode())); + String.format("Failed to initiate chunked blob upload: status %d", response.statusCode())); } String location = response.headers().get(Const.LOCATION_HEADER.toLowerCase()); if (!location.startsWith("http://") && !location.startsWith("https://")) { - location = "%s://%s/%s".formatted(getScheme(), ref.getApiRegistry(this), location.replaceFirst("^/", "")); + location = + String.format("%s://%s/%s", getScheme(), ref.getApiRegistry(this), location.replaceFirst("^/", "")); } LOG.debug("Chunked upload session location: {}", location); return location; @@ -1165,7 +1169,7 @@ private String uploadChunks(ContainerRef ref, InputStream stream, long totalSize break; } long rangeEnd = offset + read - 1; - String contentRange = "%d-%d".formatted(offset, rangeEnd); + String contentRange = String.format("%d-%d", offset, rangeEnd); final byte[] chunk = java.util.Arrays.copyOf(buffer, read); URI patchUri = URI.create(location); HttpClient.ResponseWrapper patchResponse = client.patch( @@ -1181,15 +1185,17 @@ private String uploadChunks(ContainerRef ref, InputStream stream, long totalSize authProvider); logResponse(patchResponse); if (patchResponse.statusCode() != 202) { - throw new OrasException("Chunked upload PATCH failed for range %s: status %d" - .formatted(contentRange, patchResponse.statusCode())); + throw new OrasException(String.format( + "Chunked upload PATCH failed for range %s: status %d", + contentRange, patchResponse.statusCode())); } // The registry MAY return a new location after each PATCH String newLocation = patchResponse.headers().get(Const.LOCATION_HEADER.toLowerCase()); if (newLocation != null && !newLocation.isBlank()) { if (!newLocation.startsWith("http://") && !newLocation.startsWith("https://")) { - newLocation = "%s://%s/%s" - .formatted(getScheme(), ref.getApiRegistry(this), newLocation.replaceFirst("^/", "")); + newLocation = String.format( + "%s://%s/%s", + getScheme(), ref.getApiRegistry(this), newLocation.replaceFirst("^/", "")); } location = newLocation; LOG.debug("Chunked upload location updated: {}", location); @@ -1214,7 +1220,7 @@ private void finalizeChunkedUpload(ContainerRef ref, String location, String dig logResponse(putResponse); if (putResponse.statusCode() != 201) { throw new OrasException( - "Failed to finalize chunked blob upload: status %d".formatted(putResponse.statusCode())); + String.format("Failed to finalize chunked blob upload: status %d", putResponse.statusCode())); } LOG.debug("Chunked upload finalized successfully for digest: {}", digest); } @@ -1237,7 +1243,7 @@ private HttpClient.ResponseWrapper headBlob(ContainerRef containerRef) { if (!ref.isInsecure(this) && this.isInsecure()) { return copyForNewTransport(ref.getRegistry(), false).headBlob(ref); } - URI uri = URI.create("%s://%s".formatted(getScheme(), ref.getBlobsPath(this))); + URI uri = URI.create(String.format("%s://%s", getScheme(), ref.getBlobsPath(this))); HttpClient.ResponseWrapper response = client.head( uri, Map.of(Const.ACCEPT_HEADER, Const.APPLICATION_OCTET_STREAM_HEADER_VALUE), @@ -1283,7 +1289,7 @@ private void fetchBlobDirect(ContainerRef containerRef, Path path) { asSecure().fetchBlobDirect(containerRef, path); return; } - URI uri = URI.create("%s://%s".formatted(getScheme(), ref.getBlobsPath(this))); + URI uri = URI.create(String.format("%s://%s", getScheme(), ref.getBlobsPath(this))); HttpClient.ResponseWrapper response = client.download( uri, Map.of(Const.ACCEPT_HEADER, Const.APPLICATION_OCTET_STREAM_HEADER_VALUE), @@ -1308,7 +1314,7 @@ private InputStream fetchBlobDirect(ContainerRef containerRef) { if (!ref.isInsecure(this) && this.isInsecure()) { return copyForNewTransport(ref.getRegistry(), false).fetchBlobDirect(ref); } - URI uri = URI.create("%s://%s".formatted(getScheme(), ref.getBlobsPath(this))); + URI uri = URI.create(String.format("%s://%s", getScheme(), ref.getBlobsPath(this))); HttpClient.ResponseWrapper response = client.download( uri, Map.of(Const.ACCEPT_HEADER, Const.APPLICATION_OCTET_STREAM_HEADER_VALUE), @@ -1360,9 +1366,9 @@ public Manifest getManifest(ContainerRef containerRef) { "Expected manifest but got index. Probably a multi-platform image instead of artifact"); } if (contentType.equals(Const.LEGACY_MANIFEST_MEDIA_TYPE)) { - throw new OrasException( - "Schema version 1 with manifest media type '%s' is not supported. Please use schema version 2 or higher" - .formatted(contentType)); + throw new OrasException(String.format( + "Schema version 1 with manifest media type '%s' is not supported. Please use schema version 2 or higher", + contentType)); } String json = descriptor.getJson(); String digest = descriptor.getDigest(); @@ -1388,7 +1394,7 @@ public Index getIndex(ContainerRef containerRef) { Descriptor descriptor = getDescriptor(containerRef); String contentType = descriptor.getMediaType(); if (!isIndexMediaType(contentType)) { - throw new OrasException("Expected index but got %s".formatted(contentType)); + throw new OrasException(String.format("Expected index but got %s", contentType)); } String json = descriptor.getJson(); String digest = descriptor.getDigest(); @@ -1449,7 +1455,7 @@ boolean exists(ContainerRef containerRef) { if (!ref.isInsecure(this) && this.isInsecure()) { return copyForNewTransport(ref.getRegistry(), false).exists(ref); } - URI uri = URI.create("%s://%s".formatted(getScheme(), ref.getManifestsPath(this))); + URI uri = URI.create(String.format("%s://%s", getScheme(), ref.getManifestsPath(this))); HttpClient.ResponseWrapper response = client.head(uri, Map.of(Const.ACCEPT_HEADER, Const.MANIFEST_ACCEPT_TYPE), Scopes.of(ref), authProvider); logResponse(response); @@ -1485,11 +1491,57 @@ boolean existsWithMirrorFallback(ContainerRef containerRef) { /** * A mirror registry paired with the container reference rewritten to point at it. - * @param registry The registry pointed at the mirror's location - * @param ref The container reference rewritten for the mirror */ @OrasModel - private record MirrorCandidate(Registry registry, ContainerRef ref) {} + private static final class MirrorCandidate { + + private final Registry registry; + private final ContainerRef ref; + + /** + * Create a new MirrorCandidate. + * @param registry The registry pointed at the mirror's location + * @param ref The container reference rewritten for the mirror + */ + MirrorCandidate(Registry registry, ContainerRef ref) { + this.registry = registry; + this.ref = ref; + } + + /** + * Get the registry pointed at the mirror's location. + * @return The registry + */ + Registry registry() { + return registry; + } + + /** + * Get the container reference rewritten for the mirror. + * @return The container reference + */ + ContainerRef ref() { + return ref; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MirrorCandidate)) return false; + MirrorCandidate that = (MirrorCandidate) o; + return Objects.equals(registry, that.registry) && Objects.equals(ref, that.ref); + } + + @Override + public int hashCode() { + return Objects.hash(registry, ref); + } + + @Override + public String toString() { + return "MirrorCandidate[registry=" + registry + ", ref=" + ref + "]"; + } + } /** * Resolve the mirrors applicable to the given container reference, in configured order. @@ -1557,7 +1609,7 @@ private HttpClient.ResponseWrapper getManifestResponseDirect(ContainerRe if (!ref.isInsecure(this) && this.isInsecure()) { return copyForNewTransport(ref.getRegistry(), false).getManifestResponseDirect(ref); } - URI uri = URI.create("%s://%s".formatted(getScheme(), ref.getManifestsPath(this))); + URI uri = URI.create(String.format("%s://%s", getScheme(), ref.getManifestsPath(this))); HttpClient.ResponseWrapper response = client.head(uri, Map.of(Const.ACCEPT_HEADER, Const.MANIFEST_ACCEPT_TYPE), Scopes.of(ref), authProvider); logResponse(response); @@ -1627,7 +1679,7 @@ private void ensureDigest(String expected, @Nullable String current) { throw new OrasException("Received null digest"); } if (!expected.equals(current)) { - throw new OrasException("Digest mismatch: %s != %s".formatted(expected, current)); + throw new OrasException(String.format("Digest mismatch: %s != %s", expected, current)); } } @@ -1733,7 +1785,7 @@ private void pullLayer(ContainerRef ref, Layer layer, Path path, boolean overwri LOG.trace("Actual digest: {}", actualDigest); if (!expectedDigest.equals(actualDigest)) { throw new OrasException( - "Digest mismatch: expected %s but got %s".formatted(expectedDigest, actualDigest)); + String.format("Digest mismatch: expected %s but got %s", expectedDigest, actualDigest)); } } @@ -1744,9 +1796,9 @@ private void pullLayer(ContainerRef ref, Layer layer, Path path, boolean overwri Path targetPath = path.resolve(layer.getAnnotations().get(Const.ANNOTATION_TITLE)) .normalize(); if (!targetPath.startsWith(path.normalize())) { - throw new OrasException( - "Refusing to pull layer: path is not withing folder in title annotation '%s'" - .formatted(layer.getAnnotations().get(Const.ANNOTATION_TITLE))); + throw new OrasException(String.format( + "Refusing to pull layer: path is not withing folder in title annotation '%s'", + layer.getAnnotations().get(Const.ANNOTATION_TITLE))); } if (Files.exists(targetPath) && !overwrite) { LOG.info("File already exists: {}", targetPath); @@ -1772,12 +1824,12 @@ URI createLocationWithDigest(String location, String digest) { // sometimes CI can add a query to this, so making sure we're using the right query param symbol if (uploadURI.getQuery() == null) { - uploadURI = new URI(uploadURI + "?digest=%s".formatted(digest)); + uploadURI = new URI(uploadURI + String.format("?digest=%s", digest)); } else { - uploadURI = new URI(uploadURI + "&digest=%s".formatted(digest)); + uploadURI = new URI(uploadURI + String.format("&digest=%s", digest)); } } catch (URISyntaxException e) { - throw new OrasException("Failed parse location header: %s".formatted(location)); + throw new OrasException(String.format("Failed parse location header: %s", location)); } return uploadURI; @@ -1797,7 +1849,7 @@ ResolvedRegistry getResolvedHeaders(ContainerRef containerRef) { return copyForNewTransport(ref.getRegistry(), false).getResolvedHeaders(ref); } URI uri = URI.create( - "%s://%s".formatted(getScheme(), ref.forRegistry(this).getManifestsPath(this))); + String.format("%s://%s", getScheme(), ref.forRegistry(this).getManifestsPath(this))); HttpClient.ResponseWrapper response = client.head(uri, Map.of(Const.ACCEPT_HEADER, Const.MANIFEST_ACCEPT_TYPE), Scopes.of(ref), authProvider); logResponse(response); @@ -1848,10 +1900,56 @@ private Optional getParamFromLink(HttpClient.ResponseWrapper res /** * Holds a resolved registry to avoid resolution on every request (specially like blob) - * @param registry The registry URL - * @param headers The headers to use for the registry */ - private record ResolvedRegistry(String registry, Map headers) {} + private static final class ResolvedRegistry { + + private final String registry; + private final Map headers; + + /** + * Create a new ResolvedRegistry. + * @param registry The registry URL + * @param headers The headers to use for the registry + */ + ResolvedRegistry(String registry, Map headers) { + this.registry = registry; + this.headers = headers; + } + + /** + * Get the registry URL. + * @return The registry URL + */ + String registry() { + return registry; + } + + /** + * Get the headers to use for the registry. + * @return The headers + */ + Map headers() { + return headers; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ResolvedRegistry)) return false; + ResolvedRegistry that = (ResolvedRegistry) o; + return Objects.equals(registry, that.registry) && Objects.equals(headers, that.headers); + } + + @Override + public int hashCode() { + return Objects.hash(registry, headers); + } + + @Override + public String toString() { + return "ResolvedRegistry[registry=" + registry + ", headers=" + headers + "]"; + } + } /** * Builder for the registry @@ -2222,15 +2320,24 @@ private void verify() { } verified = true; Map actualByPrefix = new HashMap<>(); - digestsByPrefix.forEach((prefix, digest) -> - actualByPrefix.put(prefix, prefix + ":" + HexFormat.of().formatHex(digest.digest()))); + digestsByPrefix.forEach( + (prefix, digest) -> actualByPrefix.put(prefix, prefix + ":" + toHexString(digest.digest()))); for (String expected : expectedDigests) { String prefix = SupportedAlgorithm.fromDigest(expected).getPrefix(); String actual = actualByPrefix.get(prefix); if (!expected.equals(actual)) { - throw new OrasException("Digest mismatch: %s != %s".formatted(expected, actual)); + throw new OrasException(String.format("Digest mismatch: %s != %s", expected, actual)); } } } } + + private static String toHexString(byte[] bytes) { + StringBuilder sb = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + sb.append(Character.forDigit((b >> 4) & 0xF, 16)); + sb.append(Character.forDigit(b & 0xF, 16)); + } + return sb.toString(); + } } diff --git a/src/main/java/land/oras/Repositories.java b/src/main/java/land/oras/Repositories.java index bfe094bf..49a08aec 100644 --- a/src/main/java/land/oras/Repositories.java +++ b/src/main/java/land/oras/Repositories.java @@ -20,13 +20,54 @@ package land.oras; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; +import java.util.Objects; import org.jspecify.annotations.NullMarked; /** * The repositories response object - * @param repositories The repositories */ @NullMarked @OrasModel -public record Repositories(List repositories) {} +public final class Repositories { + + private final List repositories; + + /** + * Create a new repositories instance + * @param repositories The repositories + */ + @JsonCreator + public Repositories(@JsonProperty("repositories") List repositories) { + this.repositories = repositories; + } + + /** + * Get the repositories + * @return The repositories + */ + @JsonProperty("repositories") + public List repositories() { + return repositories; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Repositories)) return false; + Repositories that = (Repositories) o; + return Objects.equals(repositories, that.repositories); + } + + @Override + public int hashCode() { + return Objects.hash(repositories); + } + + @Override + public String toString() { + return "Repositories[repositories=" + repositories + "]"; + } +} diff --git a/src/main/java/land/oras/Tags.java b/src/main/java/land/oras/Tags.java index 588f24cc..1b11ae9b 100644 --- a/src/main/java/land/oras/Tags.java +++ b/src/main/java/land/oras/Tags.java @@ -20,22 +20,45 @@ package land.oras; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; +import java.util.Objects; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; /** * The tags response object - * @param name The name - * @param tags The tags - * @param last The last tag index, to iterate - * @param n The n parameter, to iterate. As part of link response */ @NullMarked @OrasModel @JsonInclude(JsonInclude.Include.NON_NULL) -public record Tags(String name, List tags, @Nullable String last, @Nullable Integer n) { +public final class Tags { + + private final String name; + private final List tags; + private final @Nullable String last; + private final @Nullable Integer n; + + /** + * Constructor with all fields + * @param name The name + * @param tags The tags + * @param last The last tag index, to iterate + * @param n The n parameter, to iterate + */ + @JsonCreator + public Tags( + @JsonProperty("name") String name, + @JsonProperty("tags") List tags, + @JsonProperty("last") @Nullable String last, + @JsonProperty("n") @Nullable Integer n) { + this.name = name; + this.tags = tags; + this.last = last; + this.n = n; + } /** * Constructor without last @@ -46,6 +69,42 @@ public Tags(String name, List tags) { this(name, tags, null, null); } + /** + * Get the name + * @return The name + */ + @JsonProperty("name") + public String name() { + return name; + } + + /** + * Get the tags + * @return The tags + */ + @JsonProperty("tags") + public List tags() { + return tags; + } + + /** + * Get the last tag index + * @return The last tag index + */ + @JsonProperty("last") + public @Nullable String last() { + return last; + } + + /** + * Get the n parameter + * @return The n parameter + */ + @JsonProperty("n") + public @Nullable Integer n() { + return n; + } + /** * With last * @param last The last tag index, to iterate @@ -63,4 +122,25 @@ public Tags withLast(@Nullable String last) { public Tags withN(@Nullable Integer n) { return new Tags(this.name, this.tags, last, n); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Tags)) return false; + Tags that = (Tags) o; + return Objects.equals(name, that.name) + && Objects.equals(tags, that.tags) + && Objects.equals(last, that.last) + && Objects.equals(n, that.n); + } + + @Override + public int hashCode() { + return Objects.hash(name, tags, last, n); + } + + @Override + public String toString() { + return "Tags[name=" + name + ", tags=" + tags + ", last=" + last + ", n=" + n + "]"; + } } diff --git a/src/main/java/land/oras/auth/AbstractUsernamePasswordProvider.java b/src/main/java/land/oras/auth/AbstractUsernamePasswordProvider.java index e95912a6..b207e38d 100644 --- a/src/main/java/land/oras/auth/AbstractUsernamePasswordProvider.java +++ b/src/main/java/land/oras/auth/AbstractUsernamePasswordProvider.java @@ -26,7 +26,7 @@ /** * A provider for username and password authentication */ -public abstract sealed class AbstractUsernamePasswordProvider implements AuthProvider permits UsernamePasswordProvider { +public abstract class AbstractUsernamePasswordProvider implements AuthProvider { /** * The username diff --git a/src/main/java/land/oras/auth/AuthStore.java b/src/main/java/land/oras/auth/AuthStore.java index e8726146..d764e7cb 100644 --- a/src/main/java/land/oras/auth/AuthStore.java +++ b/src/main/java/land/oras/auth/AuthStore.java @@ -20,6 +20,7 @@ package land.oras.auth; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import java.io.IOException; import java.io.OutputStream; @@ -66,9 +67,9 @@ public class AuthStore { */ static String credentialHelperBinaryName(@Nullable String suffix) { if (suffix == null || !VALID_HELPER_SUFFIX.matcher(suffix).matches()) { - throw new OrasException( - "Invalid credential helper name '%s': only letters, digits, '_' and '-' are allowed (no path separators or '..')" - .formatted(suffix)); + throw new OrasException(String.format( + "Invalid credential helper name '%s': only letters, digits, '_' and '-' are allowed (no path separators or '..')", + suffix)); } return "docker-credential-" + suffix; } @@ -162,15 +163,58 @@ private static List defaultAuthPaths() { /** * Nested ConfigFile class to represent the configuration file. - * @param auths The auths map. - * @param credHelpers The credential helpers map. - * @param credsStore The credentials store. */ @OrasModel - record ConfigFile( - Map> auths, - @Nullable Map credHelpers, - @Nullable String credsStore) { + static final class ConfigFile { + + private final Map> auths; + private final @Nullable Map credHelpers; + private final @Nullable String credsStore; + + /** + * Constructs a new {@code ConfigFile} object. + * @param auths The auths map. + * @param credHelpers The credential helpers map. + * @param credsStore The credentials store. + */ + @JsonCreator + ConfigFile( + @JsonProperty("auths") Map> auths, + @JsonProperty("credHelpers") @Nullable Map credHelpers, + @JsonProperty("credsStore") @Nullable String credsStore) { + this.auths = auths; + this.credHelpers = credHelpers; + this.credsStore = credsStore; + } + + /** + * Returns the auths map. + * @return The auths map. + */ + @JsonProperty("auths") + Map> auths() { + return auths; + } + + /** + * Returns the credential helpers map. + * @return The credential helpers map. + */ + @JsonProperty("credHelpers") + @Nullable + Map credHelpers() { + return credHelpers; + } + + /** + * Returns the credentials store. + * @return The credentials store. + */ + @JsonProperty("credsStore") + @Nullable + String credsStore() { + return credsStore; + } /** * Constructs a new {@code ConfigFile} object with the specified auths. @@ -189,6 +233,26 @@ static ConfigFile fromCredential(Credential credential) { Map.of(), null); } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (!(o instanceof ConfigFile)) return false; + ConfigFile that = (ConfigFile) o; + return Objects.equals(auths, that.auths) + && Objects.equals(credHelpers, that.credHelpers) + && Objects.equals(credsStore, that.credsStore); + } + + @Override + public int hashCode() { + return Objects.hash(auths, credHelpers, credsStore); + } + + @Override + public String toString() { + return "ConfigFile[auths=" + auths + ", credHelpers=" + credHelpers + ", credsStore=" + credsStore + "]"; + } } /** @@ -334,8 +398,9 @@ private static Credential getFromCredentialHelper(String suffix, String hostname if (exit != 0) { String stderr = new String(proc.getErrorStream().readAllBytes(), StandardCharsets.UTF_8); String stdout = new String(proc.getInputStream().readAllBytes(), StandardCharsets.UTF_8); - String message = "Credential helper '%s' exited with code %d and error: '%s' and stdout '%s'." - .formatted(binary, exit, stderr.trim(), stdout.trim()); + String message = String.format( + "Credential helper '%s' exited with code %d and error: '%s' and stdout '%s'.", + binary, exit, stderr.trim(), stdout.trim()); LOG.warn(message); throw new OrasException(message); } @@ -355,11 +420,13 @@ private static Credential getFromCredentialHelper(String suffix, String hostname /** * Nested Credential class to represent username and password pairs. - * @param username The username for the credential. - * @param password The password for the credential. */ @OrasModel - public record Credential(String username, String password) { + public static final class Credential { + + private final String username; + private final String password; + /** * Constructs a new {@code Credential} object with the specified username and password. * @@ -370,19 +437,95 @@ public Credential(String username, String password) { this.username = Objects.requireNonNull(username, "Username cannot be null"); this.password = Objects.requireNonNull(password, "Password cannot be null"); } + + /** + * Returns the username. + * @return The username. + */ + public String username() { + return username; + } + + /** + * Returns the password. + * @return The password. + */ + public String password() { + return password; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (!(o instanceof Credential)) return false; + Credential that = (Credential) o; + return Objects.equals(username, that.username) && Objects.equals(password, that.password); + } + + @Override + public int hashCode() { + return Objects.hash(username, password); + } + + @Override + public String toString() { + return "Credential[username=" + username + ", password=" + password + "]"; + } } /** - * Credential helper response - * @param serverUrl The server URL - * @param username The username - * @param secret The secret (password or token) + * Credential helper response. */ @OrasModel - public record CredentialHelperResponse( - @JsonProperty("ServerURL") String serverUrl, - @JsonProperty("Username") String username, - @JsonProperty("Secret") String secret) { + public static final class CredentialHelperResponse { + + private final String serverUrl; + private final String username; + private final String secret; + + /** + * Constructs a new {@code CredentialHelperResponse}. + * @param serverUrl The server URL. + * @param username The username. + * @param secret The secret (password or token). + */ + @JsonCreator + public CredentialHelperResponse( + @JsonProperty("ServerURL") String serverUrl, + @JsonProperty("Username") String username, + @JsonProperty("Secret") String secret) { + this.serverUrl = serverUrl; + this.username = username; + this.secret = secret; + } + + /** + * Returns the server URL. + * @return The server URL. + */ + @JsonProperty("ServerURL") + public String serverUrl() { + return serverUrl; + } + + /** + * Returns the username. + * @return The username. + */ + @JsonProperty("Username") + public String username() { + return username; + } + + /** + * Returns the secret. + * @return The secret (password or token). + */ + @JsonProperty("Secret") + public String secret() { + return secret; + } + /** * Convert to Credential * @return Credential @@ -390,5 +533,26 @@ public record CredentialHelperResponse( public Credential asCredential() { return new Credential(username, secret); } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (!(o instanceof CredentialHelperResponse)) return false; + CredentialHelperResponse that = (CredentialHelperResponse) o; + return Objects.equals(serverUrl, that.serverUrl) + && Objects.equals(username, that.username) + && Objects.equals(secret, that.secret); + } + + @Override + public int hashCode() { + return Objects.hash(serverUrl, username, secret); + } + + @Override + public String toString() { + return "CredentialHelperResponse[serverUrl=" + serverUrl + ", username=" + username + ", secret=" + secret + + "]"; + } } } diff --git a/src/main/java/land/oras/auth/HttpClient.java b/src/main/java/land/oras/auth/HttpClient.java index 47b9301f..676eee52 100644 --- a/src/main/java/land/oras/auth/HttpClient.java +++ b/src/main/java/land/oras/auth/HttpClient.java @@ -20,6 +20,8 @@ package land.oras.auth; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.Timer; @@ -605,7 +607,7 @@ public TokenResponse refreshToken( LOG.debug("WWW-Authenticate header: realm={}, service={}, scope={}, error={}", realm, service, scope, error); - String query = "scope=%s&service=%s".formatted(scope, URLEncoder.encode(service, StandardCharsets.UTF_8)); + String query = String.format("scope=%s&service=%s", scope, URLEncoder.encode(service, StandardCharsets.UTF_8)); URI uri = URI.create(realm + "?" + query); @@ -687,13 +689,16 @@ private ResponseWrapper executeRequest( // Scopes are invariant across retries — compute once. ContainerRef containerRef = scopes.getContainerRef(); LOG.debug("Scopes are adding registry scopes"); - Scopes newScopes = - switch (method) { - case "GET", "HEAD" -> scopes.withAddedRegistryScopes(Scope.PULL); - case "POST", "PUT", "PATCH" -> scopes.withAddedRegistryScopes(Scope.PUSH); - case "DELETE" -> scopes.withAddedRegistryScopes(Scope.DELETE); - default -> throw new OrasException("Unsupported HTTP method: " + method); - }; + Scopes newScopes; + if ("GET".equals(method) || "HEAD".equals(method)) { + newScopes = scopes.withAddedRegistryScopes(Scope.PULL); + } else if ("POST".equals(method) || "PUT".equals(method) || "PATCH".equals(method)) { + newScopes = scopes.withAddedRegistryScopes(Scope.PUSH); + } else if ("DELETE".equals(method)) { + newScopes = scopes.withAddedRegistryScopes(Scope.DELETE); + } else { + throw new OrasException("Unsupported HTTP method: " + method); + } newScopes = newScopes.withIdentity(authProvider.getIdentity(containerRef)); LOG.debug("Existing scopes: {}", scopes.getScopes()); LOG.debug("New scopes: {}", newScopes.getScopes()); @@ -890,8 +895,25 @@ private ResponseWrapper redoRequest( boolean includeAuthHeaderForRedirect = isSameOrigin(originUri, redirectUri); if (!includeAuthHeaderForRedirect) { LOG.debug("Skipping auth header for redirect from {} to {}", originUri, redirectUri); - builder = HttpRequest.newBuilder( - builder.build(), (name, value) -> !name.equalsIgnoreCase(Const.AUTHORIZATION_HEADER)); + HttpRequest existingReq = builder.build(); + HttpRequest.Builder redirectBuilder = + HttpRequest.newBuilder().uri(existingReq.uri()); + existingReq.timeout().ifPresent(redirectBuilder::timeout); + existingReq.headers().map().forEach((headerName, values) -> { + if (!headerName.equalsIgnoreCase(Const.AUTHORIZATION_HEADER)) { + for (String v : values) { + redirectBuilder.header(headerName, v); + } + } + }); + if (existingReq.bodyPublisher().isPresent()) { + redirectBuilder.method( + existingReq.method(), + existingReq.bodyPublisher().get()); + } else { + redirectBuilder.method(existingReq.method(), HttpRequest.BodyPublishers.noBody()); + } + builder = redirectBuilder; } return toResponseWrapper( @@ -958,13 +980,81 @@ private void logRequest(HttpRequest request, byte[] body) { /** * Response wrapper * @param The response type - * @param response The response - * @param statusCode The status code - * @param headers The headers - * @param service The service (not on response but on HTTP headers) */ - public record ResponseWrapper( - T response, int statusCode, Map headers, @Nullable String service) {} + public static final class ResponseWrapper { + private final T response; + private final int statusCode; + private final Map headers; + private final @Nullable String service; + + /** + * Create a new response wrapper + * @param response The response + * @param statusCode The status code + * @param headers The headers + * @param service The service (not on response but on HTTP headers) + */ + public ResponseWrapper(T response, int statusCode, Map headers, @Nullable String service) { + this.response = response; + this.statusCode = statusCode; + this.headers = headers; + this.service = service; + } + + /** + * Get the response + * @return The response + */ + public T response() { + return response; + } + + /** + * Get the status code + * @return The status code + */ + public int statusCode() { + return statusCode; + } + + /** + * Get the headers + * @return The headers + */ + public Map headers() { + return headers; + } + + /** + * Get the service + * @return The service + */ + public @Nullable String service() { + return service; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ResponseWrapper that = (ResponseWrapper) o; + return statusCode == that.statusCode + && Objects.equals(response, that.response) + && Objects.equals(headers, that.headers) + && Objects.equals(service, that.service); + } + + @Override + public int hashCode() { + return Objects.hash(response, statusCode, headers, service); + } + + @Override + public String toString() { + return "ResponseWrapper[response=" + response + ", statusCode=" + statusCode + ", headers=" + headers + + ", service=" + service + "]"; + } + } /** * Insecure trust manager when skipping TLS verification @@ -996,19 +1086,81 @@ public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngi /** * The token response - * @param token The token - * @param service The service (not on response but on HTTP headers) - * @param access_token The access token - * @param expires_in The expires in - * @param issued_at The issued at */ @OrasModel - public record TokenResponse( - String token, - @Nullable String access_token, - @Nullable String service, - @Nullable Integer expires_in, - @Nullable ZonedDateTime issued_at) { + public static final class TokenResponse { + private final String token; + private final @Nullable String access_token; + private final @Nullable String service; + private final @Nullable Integer expires_in; + private final @Nullable ZonedDateTime issued_at; + + /** + * Create a new token response + * @param token The token + * @param access_token The access token + * @param service The service (not on response but on HTTP headers) + * @param expires_in The expires in + * @param issued_at The issued at + */ + @JsonCreator + public TokenResponse( + @JsonProperty("token") String token, + @JsonProperty("access_token") @Nullable String access_token, + @JsonProperty("service") @Nullable String service, + @JsonProperty("expires_in") @Nullable Integer expires_in, + @JsonProperty("issued_at") @Nullable ZonedDateTime issued_at) { + this.token = token; + this.access_token = access_token; + this.service = service; + this.expires_in = expires_in; + this.issued_at = issued_at; + } + + /** + * Get the token + * @return The token + */ + @JsonProperty("token") + public String token() { + return token; + } + + /** + * Get the access token + * @return The access token + */ + @JsonProperty("access_token") + public @Nullable String access_token() { + return access_token; + } + + /** + * Get the service + * @return The service + */ + @JsonProperty("service") + public @Nullable String service() { + return service; + } + + /** + * Get the expires in + * @return The expires in + */ + @JsonProperty("expires_in") + public @Nullable Integer expires_in() { + return expires_in; + } + + /** + * Get the issued at + * @return The issued at + */ + @JsonProperty("issued_at") + public @Nullable ZonedDateTime issued_at() { + return issued_at; + } /** * Create a new token response with the service field set @@ -1020,7 +1172,6 @@ public TokenResponse forService(String service) { } /** - * >>>>>>> 6379975 (Store token into caffeine cache (#631)) * Get the effective token * @return The effective token, which is either the access_token or the token field depending on which one is present */ @@ -1028,6 +1179,23 @@ public String getEffectiveToken() { return access_token != null ? access_token : token; } + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TokenResponse that = (TokenResponse) o; + return Objects.equals(token, that.token) + && Objects.equals(access_token, that.access_token) + && Objects.equals(service, that.service) + && Objects.equals(expires_in, that.expires_in) + && Objects.equals(issued_at, that.issued_at); + } + + @Override + public int hashCode() { + return Objects.hash(token, access_token, service, expires_in, issued_at); + } + @Override public String toString() { return "TokenResponse{" + "expires_in=" + expires_in + ", issued_at=" + issued_at + '}'; diff --git a/src/main/java/land/oras/auth/RegistriesConf.java b/src/main/java/land/oras/auth/RegistriesConf.java index 2dcbf1e1..878b13c7 100644 --- a/src/main/java/land/oras/auth/RegistriesConf.java +++ b/src/main/java/land/oras/auth/RegistriesConf.java @@ -145,15 +145,60 @@ private static List dropInFiles(Path dir) { /** * The model of a mirror entry within a [[registry]] table. - * @param location The mirror registry location (host[:port][/path]). - * @param insecure Whether the mirror is insecure. - * @param pullFromMirror Controls which pull operations may use this mirror (default: {@link PullFromMirror#ALL}). */ @OrasModel - public record MirrorConfig( - @Nullable @JsonProperty("location") String location, - @Nullable @JsonProperty("insecure") Boolean insecure, - @Nullable @JsonProperty("pull-from-mirror") PullFromMirror pullFromMirror) { + public static final class MirrorConfig { + + private final @Nullable String location; + private final @Nullable Boolean insecure; + private final @Nullable PullFromMirror pullFromMirror; + + /** + * Constructor for MirrorConfig. + * @param location The mirror registry location (host[:port][/path]). + * @param insecure Whether the mirror is insecure. + * @param pullFromMirror Controls which pull operations may use this mirror (default: {@link PullFromMirror#ALL}). + */ + @JsonCreator + public MirrorConfig( + @Nullable @JsonProperty("location") String location, + @Nullable @JsonProperty("insecure") Boolean insecure, + @Nullable @JsonProperty("pull-from-mirror") PullFromMirror pullFromMirror) { + this.location = location; + this.insecure = insecure; + this.pullFromMirror = pullFromMirror; + } + + /** + * Return the mirror registry location. + * @return the mirror registry location + */ + @Nullable + @JsonProperty("location") + public String location() { + return location; + } + + /** + * Return whether the mirror is insecure. + * @return whether the mirror is insecure + */ + @Nullable + @JsonProperty("insecure") + public Boolean insecure() { + return insecure; + } + + /** + * Return the pull-from-mirror setting. + * @return the pull-from-mirror setting + */ + @Nullable + @JsonProperty("pull-from-mirror") + public PullFromMirror pullFromMirror() { + return pullFromMirror; + } + /** * Return true if this mirror should be accessed over plain HTTP or with unverified TLS. * @return true if insecure @@ -169,25 +214,127 @@ public boolean isInsecure() { public PullFromMirror effectivePullFromMirror() { return pullFromMirror != null ? pullFromMirror : PullFromMirror.ALL; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MirrorConfig)) return false; + MirrorConfig that = (MirrorConfig) o; + return Objects.equals(location, that.location) + && Objects.equals(insecure, that.insecure) + && Objects.equals(pullFromMirror, that.pullFromMirror); + } + + @Override + public int hashCode() { + return Objects.hash(location, insecure, pullFromMirror); + } + + @Override + public String toString() { + return "MirrorConfig[location=" + location + ", insecure=" + insecure + ", pullFromMirror=" + pullFromMirror + + "]"; + } } /** * The model of the registry configuration - * @param prefix The prefix to match against container references. - * @param location The registry location - * @param blocked Whether the registry is blocked. If true, the registry is blocked and cannot be used for pulling or pushing images. - * @param insecure Whether the registry is insecure. If true, the registry is considered insecure and may allow connections over HTTP or with invalid TLS certificates. - * @param mirrorByDigestOnly If true, all mirrors for this registry are treated as digest-only (equivalent to setting pull-from-mirror=digest-only on every mirror). - * @param mirrors Ordered list of mirror entries to try before the registry location. */ @OrasModel - record RegistryConfig( - @Nullable @JsonProperty("prefix") String prefix, - @Nullable @JsonProperty("location") String location, - @Nullable @JsonProperty("blocked") Boolean blocked, - @Nullable @JsonProperty("insecure") Boolean insecure, - @Nullable @JsonProperty("mirror-by-digest-only") Boolean mirrorByDigestOnly, - @Nullable @JsonProperty("mirror") List mirrors) { + static final class RegistryConfig { + + private final @Nullable String prefix; + private final @Nullable String location; + private final @Nullable Boolean blocked; + private final @Nullable Boolean insecure; + private final @Nullable Boolean mirrorByDigestOnly; + private final @Nullable List mirrors; + + /** + * Constructor for RegistryConfig. + * @param prefix The prefix to match against container references. + * @param location The registry location + * @param blocked Whether the registry is blocked. If true, the registry is blocked and cannot be used for pulling or pushing images. + * @param insecure Whether the registry is insecure. If true, the registry is considered insecure and may allow connections over HTTP or with invalid TLS certificates. + * @param mirrorByDigestOnly If true, all mirrors for this registry are treated as digest-only (equivalent to setting pull-from-mirror=digest-only on every mirror). + * @param mirrors Ordered list of mirror entries to try before the registry location. + */ + @JsonCreator + RegistryConfig( + @Nullable @JsonProperty("prefix") String prefix, + @Nullable @JsonProperty("location") String location, + @Nullable @JsonProperty("blocked") Boolean blocked, + @Nullable @JsonProperty("insecure") Boolean insecure, + @Nullable @JsonProperty("mirror-by-digest-only") Boolean mirrorByDigestOnly, + @Nullable @JsonProperty("mirror") List mirrors) { + this.prefix = prefix; + this.location = location; + this.blocked = blocked; + this.insecure = insecure; + this.mirrorByDigestOnly = mirrorByDigestOnly; + this.mirrors = mirrors; + } + + /** + * Return the prefix to match against container references. + * @return the prefix + */ + @Nullable + @JsonProperty("prefix") + String prefix() { + return prefix; + } + + /** + * Return the registry location. + * @return the registry location + */ + @Nullable + @JsonProperty("location") + String location() { + return location; + } + + /** + * Return whether the registry is blocked. + * @return whether the registry is blocked + */ + @Nullable + @JsonProperty("blocked") + Boolean blocked() { + return blocked; + } + + /** + * Return whether the registry is insecure. + * @return whether the registry is insecure + */ + @Nullable + @JsonProperty("insecure") + Boolean insecure() { + return insecure; + } + + /** + * Return the mirror-by-digest-only setting. + * @return the mirror-by-digest-only setting + */ + @Nullable + @JsonProperty("mirror-by-digest-only") + Boolean mirrorByDigestOnly() { + return mirrorByDigestOnly; + } + + /** + * Return the list of mirror configurations. + * @return the list of mirror configurations + */ + @Nullable + @JsonProperty("mirror") + List mirrors() { + return mirrors; + } + /** * Return true if this registry is blocked and cannot be used for pulling or pushing images. * @return true if blocked @@ -211,15 +358,67 @@ public boolean isInsecure() { public boolean isMirrorByDigestOnly() { return mirrorByDigestOnly != null && mirrorByDigestOnly; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof RegistryConfig)) return false; + RegistryConfig that = (RegistryConfig) o; + return Objects.equals(prefix, that.prefix) + && Objects.equals(location, that.location) + && Objects.equals(blocked, that.blocked) + && Objects.equals(insecure, that.insecure) + && Objects.equals(mirrorByDigestOnly, that.mirrorByDigestOnly) + && Objects.equals(mirrors, that.mirrors); + } + + @Override + public int hashCode() { + return Objects.hash(prefix, location, blocked, insecure, mirrorByDigestOnly, mirrors); + } + + @Override + public String toString() { + return "RegistryConfig[prefix=" + prefix + ", location=" + location + ", blocked=" + blocked + + ", insecure=" + insecure + ", mirrorByDigestOnly=" + mirrorByDigestOnly + ", mirrors=" + mirrors + + "]"; + } } /** * The model of the parsed prefix, which contains the host and path components of the prefix. - * @param host The host component of the prefix, which can be a specific hostname or a wildcard pattern (e.g., *.example.com). - * @param path The path component of the prefix, which can be a specific path or a path prefix (e.g., namespace/repo). */ @OrasModel - record ParsedPrefix(String host, String path) { + static final class ParsedPrefix { + + private final String host; + private final String path; + + /** + * Constructor for ParsedPrefix. + * @param host The host component of the prefix, which can be a specific hostname or a wildcard pattern (e.g., *.example.com). + * @param path The path component of the prefix, which can be a specific path or a path prefix (e.g., namespace/repo). + */ + ParsedPrefix(String host, String path) { + this.host = host; + this.path = path; + } + + /** + * Return the host component of the prefix. + * @return the host + */ + String host() { + return host; + } + + /** + * Return the path component of the prefix. + * @return the path + */ + String path() { + return path; + } static ParsedPrefix parse(String prefix) { int slash = prefix.indexOf('/'); @@ -228,6 +427,24 @@ static ParsedPrefix parse(String prefix) { } return new ParsedPrefix(prefix.substring(0, slash), prefix.substring(slash + 1)); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ParsedPrefix)) return false; + ParsedPrefix that = (ParsedPrefix) o; + return Objects.equals(host, that.host) && Objects.equals(path, that.path); + } + + @Override + public int hashCode() { + return Objects.hash(host, path); + } + + @Override + public String toString() { + return "ParsedPrefix[host=" + host + ", path=" + path + "]"; + } } /** @@ -320,16 +537,96 @@ public String getKey() { /** * The model of the configuration file, which contains the list of registry configurations, aliases, and unqualified registries. - * @param registries The list of registry configurations, each containing the registry location, whether it is blocked, and whether it is insecure. - * @param aliases The map of registry aliases, where the key is the alias and the value is the actual registry URL. - * @param unqualifiedRegistries The list of unqualified registries, which are registries that can be used without specifying a registry. */ @OrasModel - record ConfigFile( - @JsonProperty("short-name-mode") @Nullable ShortNameMode shortNameMode, - @JsonProperty("registry") @Nullable List registries, - @JsonProperty("aliases") @Nullable Map aliases, - @JsonProperty("unqualified-search-registries") @Nullable List unqualifiedRegistries) {} + static final class ConfigFile { + + private final @Nullable ShortNameMode shortNameMode; + private final @Nullable List registries; + private final @Nullable Map aliases; + private final @Nullable List unqualifiedRegistries; + + /** + * Constructor for ConfigFile. + * @param shortNameMode The short name mode for the configuration. + * @param registries The list of registry configurations, each containing the registry location, whether it is blocked, and whether it is insecure. + * @param aliases The map of registry aliases, where the key is the alias and the value is the actual registry URL. + * @param unqualifiedRegistries The list of unqualified registries, which are registries that can be used without specifying a registry. + */ + @JsonCreator + ConfigFile( + @JsonProperty("short-name-mode") @Nullable ShortNameMode shortNameMode, + @JsonProperty("registry") @Nullable List registries, + @JsonProperty("aliases") @Nullable Map aliases, + @JsonProperty("unqualified-search-registries") @Nullable List unqualifiedRegistries) { + this.shortNameMode = shortNameMode; + this.registries = registries; + this.aliases = aliases; + this.unqualifiedRegistries = unqualifiedRegistries; + } + + /** + * Return the short name mode. + * @return the short name mode + */ + @JsonProperty("short-name-mode") + @Nullable + ShortNameMode shortNameMode() { + return shortNameMode; + } + + /** + * Return the list of registry configurations. + * @return the list of registry configurations + */ + @JsonProperty("registry") + @Nullable + List registries() { + return registries; + } + + /** + * Return the map of aliases. + * @return the map of aliases + */ + @JsonProperty("aliases") + @Nullable + Map aliases() { + return aliases; + } + + /** + * Return the list of unqualified registries. + * @return the list of unqualified registries + */ + @JsonProperty("unqualified-search-registries") + @Nullable + List unqualifiedRegistries() { + return unqualifiedRegistries; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ConfigFile)) return false; + ConfigFile that = (ConfigFile) o; + return Objects.equals(shortNameMode, that.shortNameMode) + && Objects.equals(registries, that.registries) + && Objects.equals(aliases, that.aliases) + && Objects.equals(unqualifiedRegistries, that.unqualifiedRegistries); + } + + @Override + public int hashCode() { + return Objects.hash(shortNameMode, registries, aliases, unqualifiedRegistries); + } + + @Override + public String toString() { + return "ConfigFile[shortNameMode=" + shortNameMode + ", registries=" + registries + ", aliases=" + aliases + + ", unqualifiedRegistries=" + unqualifiedRegistries + "]"; + } + } /** * Get the list of unqualified registries. @@ -510,11 +807,13 @@ public List getApplicableMirrors(ContainerRef ref) { .filter(mirror -> { PullFromMirror effective = registryDigestOnly ? PullFromMirror.DIGEST_ONLY : mirror.effectivePullFromMirror(); - return switch (effective) { - case DIGEST_ONLY -> refHasDigest; - case TAG_ONLY -> refHasTag; - case ALL -> true; - }; + if (effective == PullFromMirror.DIGEST_ONLY) { + return refHasDigest; + } else if (effective == PullFromMirror.TAG_ONLY) { + return refHasTag; + } else { + return true; + } }) .collect(Collectors.toUnmodifiableList()); } diff --git a/src/main/java/land/oras/auth/Scopes.java b/src/main/java/land/oras/auth/Scopes.java index 051c08d9..f556ec59 100644 --- a/src/main/java/land/oras/auth/Scopes.java +++ b/src/main/java/land/oras/auth/Scopes.java @@ -23,6 +23,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; import land.oras.ContainerRef; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -147,7 +148,7 @@ public Scopes withAddedGlobalScopes(String... globalScopes) { containerRef, service, identity, - newScopes.stream().sorted().distinct().toList()); + newScopes.stream().sorted().distinct().collect(Collectors.toList())); } /** @@ -159,7 +160,7 @@ public Scopes withOnlyGlobalScopes() { .filter(s -> !s.startsWith("repository:") && !s.startsWith("registry:")) .sorted() .distinct() - .toList(); + .collect(Collectors.toList()); return new Scopes(containerRef, service, identity, globalScopes); } @@ -172,7 +173,7 @@ public Scopes withoutGlobalScopes() { .filter(s -> s.startsWith("repository:") || s.startsWith("registry:")) .sorted() .distinct() - .toList(); + .collect(Collectors.toList()); return new Scopes(containerRef, service, identity, nonGlobalScopes); } diff --git a/src/main/java/land/oras/exception/Error.java b/src/main/java/land/oras/exception/Error.java index 8bdc46c1..09c02831 100644 --- a/src/main/java/land/oras/exception/Error.java +++ b/src/main/java/land/oras/exception/Error.java @@ -20,16 +20,84 @@ package land.oras.exception; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Objects; import land.oras.OrasModel; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; /** * An error object for OCI API - * @param code The error code - * @param message The error message - * @param details The error details */ @NullMarked @OrasModel -public record Error(String code, String message, @Nullable String details) {} +public final class Error { + + private final String code; + private final String message; + private final @Nullable String details; + + /** + * Create a new error instance + * @param code The error code + * @param message The error message + * @param details The error details + */ + @JsonCreator + public Error( + @JsonProperty("code") String code, + @JsonProperty("message") String message, + @JsonProperty("details") @Nullable String details) { + this.code = code; + this.message = message; + this.details = details; + } + + /** + * Get the error code + * @return The error code + */ + @JsonProperty("code") + public String code() { + return code; + } + + /** + * Get the error message + * @return The error message + */ + @JsonProperty("message") + public String message() { + return message; + } + + /** + * Get the error details + * @return The error details + */ + @JsonProperty("details") + public @Nullable String details() { + return details; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Error)) return false; + Error that = (Error) o; + return Objects.equals(code, that.code) + && Objects.equals(message, that.message) + && Objects.equals(details, that.details); + } + + @Override + public int hashCode() { + return Objects.hash(code, message, details); + } + + @Override + public String toString() { + return "Error[code=" + code + ", message=" + message + ", details=" + details + "]"; + } +} diff --git a/src/main/java/land/oras/policy/ContainersPolicy.java b/src/main/java/land/oras/policy/ContainersPolicy.java index 967ff1cf..f03b3eed 100644 --- a/src/main/java/land/oras/policy/ContainersPolicy.java +++ b/src/main/java/land/oras/policy/ContainersPolicy.java @@ -22,12 +22,15 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import land.oras.OrasModel; import land.oras.exception.OrasException; import land.oras.utils.JsonUtils; @@ -35,8 +38,6 @@ import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.json.JsonMapper; /** * Represents the containers trust policy loaded from a {@code policy.json} file. @@ -62,7 +63,13 @@ public class ContainersPolicy { * that would interfere with the {@code @JsonTypeInfo} resolution here, so we use a separate * instance. */ - static final ObjectMapper POLICY_MAPPER = JsonMapper.builder().build(); + static final ObjectMapper POLICY_MAPPER; + + static { + POLICY_MAPPER = JsonMapper.builder().build(); + POLICY_MAPPER.configure( + com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } private final PolicyFile policyFile; @@ -152,8 +159,9 @@ public void verify(PolicyContext context) { List requirements = resolveRequirements(context.getTransport(), context.getScope()); for (PolicyRequirement req : requirements) { if (!req.verify(context)) { - throw new OrasException("Image '%s' rejected by containers policy requirement '%s'" - .formatted(context.getReference(), req.getType())); + throw new OrasException(String.format( + "Image '%s' rejected by containers policy requirement '%s'", + context.getReference(), req.getType())); } } LOG.debug("Policy verification passed for {}", context.getReference()); @@ -279,15 +287,45 @@ private static List defaultPolicyPaths() { /** * The raw JSON model for a {@code policy.json} file. - * - * @param defaultRequirements the mandatory global default requirement list (key - * {@code "default"} in JSON). - * @param transports optional per-transport requirement map. */ @OrasModel - record PolicyFile( - List defaultRequirements, - Map>> transports) { + static final class PolicyFile { + + private final List defaultRequirements; + private final Map>> transports; + + /** + * Construct a new PolicyFile. + * + * @param defaultRequirements the mandatory global default requirement list. + * @param transports the per-transport requirement map. + */ + PolicyFile( + List defaultRequirements, + Map>> transports) { + this.defaultRequirements = defaultRequirements; + this.transports = transports; + } + + /** + * Return the default requirements. + * + * @return the default requirements. + */ + @JsonProperty("default") + List defaultRequirements() { + return defaultRequirements; + } + + /** + * Return the transports. + * + * @return the transports. + */ + @JsonProperty("transports") + Map>> transports() { + return transports; + } /** * Deserialize a {@link PolicyFile} from its JSON form, mapping the raw transport keys to the @@ -311,5 +349,24 @@ static PolicyFile fromJson( } return new PolicyFile(defaults, byTransport); } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (!(o instanceof PolicyFile)) return false; + PolicyFile that = (PolicyFile) o; + return Objects.equals(defaultRequirements, that.defaultRequirements) + && Objects.equals(transports, that.transports); + } + + @Override + public int hashCode() { + return Objects.hash(defaultRequirements, transports); + } + + @Override + public String toString() { + return "PolicyFile[defaultRequirements=" + defaultRequirements + ", transports=" + transports + "]"; + } } } diff --git a/src/main/java/land/oras/policy/PolicyRequirement.java b/src/main/java/land/oras/policy/PolicyRequirement.java index 456a3cdf..345cc5e8 100644 --- a/src/main/java/land/oras/policy/PolicyRequirement.java +++ b/src/main/java/land/oras/policy/PolicyRequirement.java @@ -46,11 +46,7 @@ @JsonSubTypes.Type(value = PolicyRequirement.SigstoreSigned.class, name = PolicyRequirement.SigstoreSigned.TYPE), }) @OrasModel -public abstract sealed class PolicyRequirement - permits PolicyRequirement.InsecureAcceptAnything, - PolicyRequirement.Reject, - PolicyRequirement.SignedBy, - PolicyRequirement.SigstoreSigned { +public abstract class PolicyRequirement { /** * Private constructor diff --git a/src/main/java/land/oras/policy/SigstoreVerifier.java b/src/main/java/land/oras/policy/SigstoreVerifier.java index a38b6017..c51726fb 100644 --- a/src/main/java/land/oras/policy/SigstoreVerifier.java +++ b/src/main/java/land/oras/policy/SigstoreVerifier.java @@ -20,7 +20,10 @@ package land.oras.policy; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -33,6 +36,7 @@ import java.util.Base64; import java.util.List; import java.util.Map; +import java.util.Objects; import land.oras.OrasModel; import land.oras.exception.OrasException; import land.oras.utils.Const; @@ -40,8 +44,6 @@ import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.json.JsonMapper; /** * Verifies keyed Sigstore bundles attached to @@ -79,7 +81,12 @@ final class SigstoreVerifier { /** * The JSON mapper for parsing Sigstore bundles and in-toto statements. */ - private static final ObjectMapper MAPPER = JsonMapper.builder().build(); + private static final ObjectMapper MAPPER; + + static { + MAPPER = JsonMapper.builder().build(); + MAPPER.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } /** * DSSE prefix as byte @@ -187,7 +194,13 @@ static boolean verifyBundle(byte[] bundleBytes, String imageDigest, PublicKey tr * digest in one of its subjects. */ private static boolean digestMatches(byte[] payload, String imageDigest) { - InTotoStatement statement = MAPPER.readValue(payload, InTotoStatement.class); + InTotoStatement statement; + try { + statement = MAPPER.readValue(payload, InTotoStatement.class); + } catch (IOException e) { + LOG.warn("Failed to parse in-toto statement: {}", e.getMessage()); + return false; + } if (statement.subject() == null) { LOG.warn("Sigstore bundle in-toto statement does not contain any subjects"); return false; @@ -213,11 +226,12 @@ private static boolean digestMatches(byte[] payload, String imageDigest) { */ private static boolean verifySignature(PublicKey key, byte[] content, byte[] signature) { // Might support other algorithm in the future - String algorithm = - switch (key.getAlgorithm()) { - case Const.KEY_EC_ALGORITHM -> Const.KEY_SHA256_ECDSA_SIGNATURE_ALGORITHM; - default -> null; - }; + String algorithm; + if (Const.KEY_EC_ALGORITHM.equals(key.getAlgorithm())) { + algorithm = Const.KEY_SHA256_ECDSA_SIGNATURE_ALGORITHM; + } else { + algorithm = null; + } if (algorithm == null) { LOG.warn("Unsupported public key algorithm for Sigstore verification: {}", key.getAlgorithm()); return false; @@ -365,38 +379,273 @@ static List loadKeys(PolicyRequirement.SigstoreSigned requirement) { * Only the DSSE envelope is modeled; verification material (tlog/timestamps) is ignored. */ @OrasModel - record Bundle(@JsonProperty("dsseEnvelope") @Nullable DsseEnvelope dsseEnvelope) {} + static final class Bundle { + + private final @Nullable DsseEnvelope dsseEnvelope; + + /** + * Construct a new Bundle. + * + * @param dsseEnvelope the DSSE envelope. + */ + @JsonCreator + Bundle(@JsonProperty("dsseEnvelope") @Nullable DsseEnvelope dsseEnvelope) { + this.dsseEnvelope = dsseEnvelope; + } + + /** + * Return the DSSE envelope. + * + * @return the DSSE envelope. + */ + @JsonProperty("dsseEnvelope") + @Nullable + DsseEnvelope dsseEnvelope() { + return dsseEnvelope; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (!(o instanceof Bundle)) return false; + Bundle that = (Bundle) o; + return Objects.equals(dsseEnvelope, that.dsseEnvelope); + } + + @Override + public int hashCode() { + return Objects.hash(dsseEnvelope); + } + + @Override + public String toString() { + return "Bundle[dsseEnvelope=" + dsseEnvelope + "]"; + } + } /** * DSSE envelope containing a base64-encoded payload and one or more signatures. - * @param payload The payload - * @param payloadType Type of payload - * @param signatures List of signatures */ @OrasModel - record DsseEnvelope( - @JsonProperty("payload") @Nullable String payload, - @JsonProperty("payloadType") @Nullable String payloadType, - @JsonProperty("signatures") @Nullable List signatures) {} + static final class DsseEnvelope { + + private final @Nullable String payload; + private final @Nullable String payloadType; + private final @Nullable List signatures; + + /** + * Construct a new DsseEnvelope. + * + * @param payload The payload. + * @param payloadType Type of payload. + * @param signatures List of signatures. + */ + @JsonCreator + DsseEnvelope( + @JsonProperty("payload") @Nullable String payload, + @JsonProperty("payloadType") @Nullable String payloadType, + @JsonProperty("signatures") @Nullable List signatures) { + this.payload = payload; + this.payloadType = payloadType; + this.signatures = signatures; + } + + /** + * Return the payload. + * + * @return the payload. + */ + @JsonProperty("payload") + @Nullable + String payload() { + return payload; + } + + /** + * Return the payload type. + * + * @return the payload type. + */ + @JsonProperty("payloadType") + @Nullable + String payloadType() { + return payloadType; + } + + /** + * Return the signatures. + * + * @return the signatures. + */ + @JsonProperty("signatures") + @Nullable + List signatures() { + return signatures; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (!(o instanceof DsseEnvelope)) return false; + DsseEnvelope that = (DsseEnvelope) o; + return Objects.equals(payload, that.payload) + && Objects.equals(payloadType, that.payloadType) + && Objects.equals(signatures, that.signatures); + } + + @Override + public int hashCode() { + return Objects.hash(payload, payloadType, signatures); + } + + @Override + public String toString() { + return "DsseEnvelope[payload=" + payload + ", payloadType=" + payloadType + ", signatures=" + signatures + + "]"; + } + } /** * A single DSSE signature (base64-encoded, DER for ECDSA) - * @param sig Signature */ @OrasModel - record DsseSignature(@JsonProperty("sig") @Nullable String sig) {} + static final class DsseSignature { + + private final @Nullable String sig; + + /** + * Construct a new DsseSignature. + * + * @param sig Signature. + */ + @JsonCreator + DsseSignature(@JsonProperty("sig") @Nullable String sig) { + this.sig = sig; + } + + /** + * Return the signature. + * + * @return the signature. + */ + @JsonProperty("sig") + @Nullable + String sig() { + return sig; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (!(o instanceof DsseSignature)) return false; + DsseSignature that = (DsseSignature) o; + return Objects.equals(sig, that.sig); + } + + @Override + public int hashCode() { + return Objects.hash(sig); + } + + @Override + public String toString() { + return "DsseSignature[sig=" + sig + "]"; + } + } /** * Minimal model of an in-toto statement, capturing only its subjects - * @param subject The subject */ @OrasModel - record InTotoStatement(@JsonProperty("subject") @Nullable List subject) {} + static final class InTotoStatement { + + private final @Nullable List subject; + + /** + * Construct a new InTotoStatement. + * + * @param subject The subject. + */ + @JsonCreator + InTotoStatement(@JsonProperty("subject") @Nullable List subject) { + this.subject = subject; + } + + /** + * Return the subject. + * + * @return the subject. + */ + @JsonProperty("subject") + @Nullable + List subject() { + return subject; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (!(o instanceof InTotoStatement)) return false; + InTotoStatement that = (InTotoStatement) o; + return Objects.equals(subject, that.subject); + } + + @Override + public int hashCode() { + return Objects.hash(subject); + } + + @Override + public String toString() { + return "InTotoStatement[subject=" + subject + "]"; + } + } /** * An in-toto subject: a map of digest algorithm to hex value - * @param digest The digest */ @OrasModel - record Subject(@JsonProperty("digest") @Nullable Map digest) {} + static final class Subject { + + private final @Nullable Map digest; + + /** + * Construct a new Subject. + * + * @param digest The digest. + */ + @JsonCreator + Subject(@JsonProperty("digest") @Nullable Map digest) { + this.digest = digest; + } + + /** + * Return the digest. + * + * @return the digest. + */ + @JsonProperty("digest") + @Nullable + Map digest() { + return digest; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (!(o instanceof Subject)) return false; + Subject that = (Subject) o; + return Objects.equals(digest, that.digest); + } + + @Override + public int hashCode() { + return Objects.hash(digest); + } + + @Override + public String toString() { + return "Subject[digest=" + digest + "]"; + } + } } diff --git a/src/main/java/land/oras/utils/Const.java b/src/main/java/land/oras/utils/Const.java index e2befe8e..369c02c2 100644 --- a/src/main/java/land/oras/utils/Const.java +++ b/src/main/java/land/oras/utils/Const.java @@ -217,13 +217,13 @@ private Const() { /** * The default accept type for the manifest */ - public static final String MANIFEST_ACCEPT_TYPE = "%s, %s, %s, %s, %s" - .formatted( - DEFAULT_INDEX_MEDIA_TYPE, - DEFAULT_MANIFEST_MEDIA_TYPE, - ARTIFACT_MANIFEST_MEDIA_TYPE, - DOCKER_INDEX_MEDIA_TYPE, - DOCKER_MANIFEST_MEDIA_TYPE); + public static final String MANIFEST_ACCEPT_TYPE = String.format( + "%s, %s, %s, %s, %s", + DEFAULT_INDEX_MEDIA_TYPE, + DEFAULT_MANIFEST_MEDIA_TYPE, + ARTIFACT_MANIFEST_MEDIA_TYPE, + DOCKER_INDEX_MEDIA_TYPE, + DOCKER_MANIFEST_MEDIA_TYPE); /** * Annotation for the title diff --git a/src/main/java/land/oras/utils/DigestUtils.java b/src/main/java/land/oras/utils/DigestUtils.java index f24d96a6..6e541aa9 100644 --- a/src/main/java/land/oras/utils/DigestUtils.java +++ b/src/main/java/land/oras/utils/DigestUtils.java @@ -27,7 +27,6 @@ import java.nio.file.StandardOpenOption; import java.security.MessageDigest; import java.security.Security; -import java.util.HexFormat; import land.oras.exception.OrasException; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.jspecify.annotations.NullMarked; @@ -38,8 +37,6 @@ @NullMarked final class DigestUtils { - private static final HexFormat HEX_FORMAT = HexFormat.of(); - static { Security.addProvider(new BouncyCastleProvider()); } @@ -118,8 +115,16 @@ static String digest(String algorithm, String prefix, InputStream input) { } } + static String toHexString(byte[] bytes) { + StringBuilder sb = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + sb.append(Character.forDigit((b >> 4) & 0xF, 16)); + sb.append(Character.forDigit(b & 0xF, 16)); + } + return sb.toString(); + } + private static String formatHex(String prefix, final byte[] hashBytes) { - String formatHex = HEX_FORMAT.formatHex(hashBytes); - return prefix + ":" + formatHex; + return prefix + ":" + toHexString(hashBytes); } } diff --git a/src/main/java/land/oras/utils/JsonUtils.java b/src/main/java/land/oras/utils/JsonUtils.java index df626aa2..7274a20e 100644 --- a/src/main/java/land/oras/utils/JsonUtils.java +++ b/src/main/java/land/oras/utils/JsonUtils.java @@ -21,6 +21,10 @@ package land.oras.utils; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import java.io.IOException; import java.io.InputStream; import java.io.Reader; @@ -30,13 +34,10 @@ import java.nio.file.Path; import land.oras.exception.OrasException; import org.jspecify.annotations.NullMarked; -import tools.jackson.core.JacksonException; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.json.JsonMapper; /** * Utility class for JSON operations. - * Use Jackson 3 internally for JSON operations + * Use Jackson internally for JSON operations */ @NullMarked public final class JsonUtils { @@ -54,9 +55,11 @@ private JsonUtils() { } static { - jsonMapper = JsonMapper.builder() - .changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.NON_EMPTY)) - .build(); + jsonMapper = JsonMapper.builder().build(); + jsonMapper.setDefaultPropertyInclusion( + JsonInclude.Value.construct(JsonInclude.Include.NON_EMPTY, JsonInclude.Include.USE_DEFAULTS)); + jsonMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + jsonMapper.registerModule(new JavaTimeModule()); } /** @@ -67,7 +70,7 @@ private JsonUtils() { public static String toJson(Object object) { try { return jsonMapper.writeValueAsString(object); - } catch (JacksonException e) { + } catch (IOException e) { throw new OrasException("Unable to convert object to JSON string", e); } } @@ -82,7 +85,7 @@ public static String toJson(Object object) { public static T fromJson(String json, Class clazz) { try { return jsonMapper.readValue(json, clazz); - } catch (JacksonException e) { + } catch (IOException e) { throw new OrasException("Unable to parse JSON string", e); } } @@ -97,7 +100,7 @@ public static T fromJson(String json, Class clazz) { public static T fromJson(InputStream is, Class clazz) { try { return jsonMapper.readValue(is, clazz); - } catch (JacksonException e) { + } catch (IOException e) { throw new OrasException("Unable to parse JSON string", e); } } @@ -115,8 +118,6 @@ public static T fromJson(Path path, Class clazz) { try { return jsonMapper.readValue(Files.readString(path, StandardCharsets.UTF_8), clazz); } catch (IOException e) { - throw new OrasException("Unable to read JSON file due to IO error", e); - } catch (JacksonException e) { throw new OrasException("Unable to parse JSON file", e); } } @@ -136,8 +137,6 @@ public static T fromJson(Path path, Type type) { Files.readString(path, StandardCharsets.UTF_8), jsonMapper.getTypeFactory().constructType(type)); } catch (IOException e) { - throw new OrasException("Unable to read JSON file due to IO error", e); - } catch (JacksonException e) { throw new OrasException("Unable to parse JSON file", e); } } @@ -154,7 +153,7 @@ public static T fromJson(Path path, Type type) { public static T fromJson(Reader reader, Type type) { try { return jsonMapper.readValue(reader, jsonMapper.getTypeFactory().constructType(type)); - } catch (JacksonException e) { + } catch (IOException e) { throw new OrasException("Unable to parse JSON content", e); } } diff --git a/src/main/java/land/oras/utils/SupportedAlgorithm.java b/src/main/java/land/oras/utils/SupportedAlgorithm.java index 9a53a834..043387df 100644 --- a/src/main/java/land/oras/utils/SupportedAlgorithm.java +++ b/src/main/java/land/oras/utils/SupportedAlgorithm.java @@ -168,8 +168,9 @@ public static boolean isSupported(String digest) { // Check the size String value = digest.substring(algorithm.getPrefix().length() + 1); if (value.length() != algorithm.getSize() * 2) { - throw new OrasException("Invalid digest %s, expected size is %d, but got %d" - .formatted(digest, algorithm.getSize(), value.length())); + throw new OrasException(String.format( + "Invalid digest %s, expected size is %d, but got %d", + digest, algorithm.getSize(), value.length())); } return true; } diff --git a/src/main/java/land/oras/utils/TomlUtils.java b/src/main/java/land/oras/utils/TomlUtils.java index 642428cc..ab3d97e8 100644 --- a/src/main/java/land/oras/utils/TomlUtils.java +++ b/src/main/java/land/oras/utils/TomlUtils.java @@ -20,19 +20,18 @@ package land.oras.utils; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.toml.TomlMapper; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import land.oras.exception.OrasException; import org.jspecify.annotations.NullMarked; -import tools.jackson.core.JacksonException; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.dataformat.toml.TomlMapper; /** * Utility class for TOML operations. - * Use Jackson 3 internally for TOML operations + * Use Jackson internally for TOML operations */ @NullMarked public final class TomlUtils { @@ -61,7 +60,7 @@ private TomlUtils() { public static String toToml(Object object) { try { return tomlMapper.writeValueAsString(object); - } catch (JacksonException e) { + } catch (IOException e) { throw new OrasException("Unable to convert object to TOML string", e); } } @@ -76,7 +75,7 @@ public static String toToml(Object object) { public static T fromToml(String toml, Class clazz) { try { return tomlMapper.readValue(toml, clazz); - } catch (JacksonException e) { + } catch (IOException e) { throw new OrasException("Unable to parse TOML string", e); } } @@ -92,7 +91,7 @@ public static T fromToml(String toml, Class clazz) { public static T fromToml(Path path, Class clazz) { try { return tomlMapper.readValue(Files.readString(path, StandardCharsets.UTF_8), clazz); - } catch (IOException | JacksonException e) { + } catch (IOException e) { throw new OrasException("Unable to read TOML from file", e); } } diff --git a/src/main/java/land/oras/utils/YamlUtils.java b/src/main/java/land/oras/utils/YamlUtils.java index 20aa7075..478243ef 100644 --- a/src/main/java/land/oras/utils/YamlUtils.java +++ b/src/main/java/land/oras/utils/YamlUtils.java @@ -20,19 +20,18 @@ package land.oras.utils; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import land.oras.exception.OrasException; import org.jspecify.annotations.NullMarked; -import tools.jackson.core.JacksonException; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.dataformat.yaml.YAMLMapper; /** * Utility class for YAML operations. - * Use Jackson 3 internally for YAML operations + * Use Jackson internally for YAML operations */ @NullMarked public final class YamlUtils { @@ -61,7 +60,7 @@ private YamlUtils() { public static String toYaml(Object object) { try { return yamlMapper.writeValueAsString(object); - } catch (JacksonException e) { + } catch (IOException e) { throw new OrasException("Unable to convert object to YAML string", e); } } @@ -76,7 +75,7 @@ public static String toYaml(Object object) { public static T fromYaml(String yaml, Class clazz) { try { return yamlMapper.readValue(yaml, clazz); - } catch (JacksonException e) { + } catch (IOException e) { throw new OrasException("Unable to parse YAML string", e); } } @@ -92,7 +91,7 @@ public static T fromYaml(String yaml, Class clazz) { public static T fromYaml(Path path, Class clazz) { try { return yamlMapper.readValue(Files.readString(path, StandardCharsets.UTF_8), clazz); - } catch (IOException | JacksonException e) { + } catch (IOException e) { throw new OrasException("Unable to read YAML from file", e); } } diff --git a/src/test/java/land/oras/AnnotationsTest.java b/src/test/java/land/oras/AnnotationsTest.java index ed1982c0..a9c7adbd 100644 --- a/src/test/java/land/oras/AnnotationsTest.java +++ b/src/test/java/land/oras/AnnotationsTest.java @@ -100,18 +100,16 @@ void testToString() { } private String sampleAnnotations() { - return """ - { - "$config": { - "hello": "world" - }, - "$manifest": { - "foo": "bar" - }, - "cake.txt": { - "fun": "more cream" - } - } - """; + return " {\n" + + " \"$config\": {\n" + + " \"hello\": \"world\"\n" + + " },\n" + + " \"$manifest\": {\n" + + " \"foo\": \"bar\"\n" + + " },\n" + + " \"cake.txt\": {\n" + + " \"fun\": \"more cream\"\n" + + " }\n" + + " }\n"; } } diff --git a/src/test/java/land/oras/ContainerRefTest.java b/src/test/java/land/oras/ContainerRefTest.java index 559324f4..41e0942a 100644 --- a/src/test/java/land/oras/ContainerRefTest.java +++ b/src/test/java/land/oras/ContainerRefTest.java @@ -40,20 +40,17 @@ class ContainerRefTest { @Execution(ExecutionMode.SAME_THREAD) void shouldReadRegistriesConfig(@TempDir Path homeDir) throws Exception { // language=toml - String config = - """ - [[registry]] - location = "public.ecr.aws" - blocked = true - - [[registry]] - location = "docker.io/library/alpine" - blocked = true - - [[registry]] - location = "localhost:5000" - insecure = true - """; + String config = "[[registry]]\n" + + "location = \"public.ecr.aws\"\n" + + "blocked = true\n" + + "\n" + + "[[registry]]\n" + + "location = \"docker.io/library/alpine\"\n" + + "blocked = true\n" + + "\n" + + "[[registry]]\n" + + "location = \"localhost:5000\"\n" + + "insecure = true\n"; TestUtils.createRegistriesConfFile(homeDir, config); @@ -82,12 +79,9 @@ void shouldReadRegistriesConfig(@TempDir Path homeDir) throws Exception { void shouldDetermineFromAlias(@TempDir Path homeDir) throws Exception { // language=toml - String config = - """ - [aliases] - "my-library/my-namespace"="localhost:5000/test" - "my-library"="localhost:5000/test2" - """; + String config = "[aliases]\n" + + "\"my-library/my-namespace\"=\"localhost:5000/test\"\n" + + "\"my-library\"=\"localhost:5000/test2\"\n"; TestUtils.createRegistriesConfFile(homeDir, config); @@ -105,16 +99,13 @@ void shouldDetermineFromAlias(@TempDir Path homeDir) throws Exception { void shouldRewriteAllSubdomainToLocalProxy(@TempDir Path homeDir) throws Exception { // language=toml - String config = - """ - [[registry]] - prefix = "*.example.com" - location = "localhost:5000/example-com" - - [[registry]] - prefix = "*.otherexample.io/library" - location = "localhost:5001/docker" - """; + String config = "[[registry]]\n" + + "prefix = \"*.example.com\"\n" + + "location = \"localhost:5000/example-com\"\n" + + "\n" + + "[[registry]]\n" + + "prefix = \"*.otherexample.io/library\"\n" + + "location = \"localhost:5001/docker\"\n"; TestUtils.createRegistriesConfFile(homeDir, config); diff --git a/src/test/java/land/oras/DockerIoITCase.java b/src/test/java/land/oras/DockerIoITCase.java index 2251da8c..945c7e35 100644 --- a/src/test/java/land/oras/DockerIoITCase.java +++ b/src/test/java/land/oras/DockerIoITCase.java @@ -116,7 +116,7 @@ void shouldCopyTagToInternalRegistry() { ContainerRef containerSource = ContainerRef.parse("docker.io/library/alpine:latest"); ContainerRef containerTarget = - ContainerRef.parse("%s/docker/library/alpine:latest".formatted(unsecureRegistry.getRegistry())); + ContainerRef.parse(String.format("%s/docker/library/alpine:latest", unsecureRegistry.getRegistry())); CopyUtils.copy(sourceRegistry, containerSource, targetRegistry, containerTarget, CopyUtils.CopyOptions.deep()); assertTrue(targetRegistry.exists(containerTarget)); @@ -149,11 +149,7 @@ void shouldContainsCommonLinuxPlatform() { void shouldCopyTagToInternalRegistryViaAlias(@TempDir Path homeDir) throws Exception { // language=toml - String config = - """ - [aliases] - "dockerhub-alpine" = "docker.io/library/alpine" - """; + String config = "[aliases]\n" + "\"dockerhub-alpine\" = \"docker.io/library/alpine\"\n"; TestUtils.createRegistriesConfFile(homeDir, config); TestUtils.withHome(homeDir, () -> { @@ -167,8 +163,8 @@ void shouldCopyTagToInternalRegistryViaAlias(@TempDir Path homeDir) throws Excep .build(); ContainerRef containerSource = ContainerRef.parse("dockerhub-alpine"); - ContainerRef containerTarget = - ContainerRef.parse("%s/docker/library/alpine:latest".formatted(unsecureRegistry.getRegistry())); + ContainerRef containerTarget = ContainerRef.parse( + String.format("%s/docker/library/alpine:latest", unsecureRegistry.getRegistry())); CopyUtils.copy( sourceRegistry, containerSource, targetRegistry, containerTarget, CopyUtils.CopyOptions.deep()); diff --git a/src/test/java/land/oras/GitHubContainerRegistryITCase.java b/src/test/java/land/oras/GitHubContainerRegistryITCase.java index da99aad3..516efc9c 100644 --- a/src/test/java/land/oras/GitHubContainerRegistryITCase.java +++ b/src/test/java/land/oras/GitHubContainerRegistryITCase.java @@ -85,27 +85,21 @@ void shouldPullSignedImage(@TempDir Path homeDir) throws Exception { Path publicKeyPath = Path.of("src/test/resources/keys/sigstore/alpine-signed.pub"); // language=toml - String config = - """ - [[registry]] - location = "ghcr.io" - insecure = false - """; + String config = "[[registry]]\n" + "location = \"ghcr.io\"\n" + "insecure = false\n"; // language=json Files.writeString( path, - """ - { - "default": [{"type": "reject"}], - "transports": { - "docker": { - "ghcr.io/jonesbusy/alpine-signed": [{"type": "sigstoreSigned", "keyPath": "%s"}] - } - } - } - """ - .formatted(publicKeyPath.toAbsolutePath().toString())); + String.format( + "{\n" + + " \"default\": [{\"type\": \"reject\"}],\n" + + " \"transports\": {\n" + + " \"docker\": {\n" + + " \"ghcr.io/jonesbusy/alpine-signed\": [{\"type\": \"sigstoreSigned\", \"keyPath\": \"%s\"}]\n" + + " }\n" + + " }\n" + + "}\n", + publicKeyPath.toAbsolutePath().toString())); ContainersPolicy policy = ContainersPolicy.newPolicy(path); TestUtils.createRegistriesConfFile(homeDir, config); TestUtils.withHome(homeDir, () -> { @@ -120,11 +114,7 @@ void shouldPullSignedImage(@TempDir Path homeDir) throws Exception { @Execution(ExecutionMode.SAME_THREAD) void shouldPullIndexWithAlias(@TempDir Path homeDir) throws Exception { // language=toml - String config = - """ - [aliases] - "oras"="ghcr.io/oras-project/oras" - """; + String config = "[aliases]\n" + "\"oras\"=\"ghcr.io/oras-project/oras\"\n"; // Setup TestUtils.createRegistriesConfFile(homeDir, config); @@ -182,7 +172,7 @@ void shouldCopyTagToInternalRegistry() { ContainerRef containerSource = ContainerRef.parse("ghcr.io/oras-project/oras:main"); ContainerRef containerTarget = - ContainerRef.parse("%s/docker/library/oras:main".formatted(unsecureRegistry.getRegistry())); + ContainerRef.parse(String.format("%s/docker/library/oras:main", unsecureRegistry.getRegistry())); CopyUtils.copy(sourceRegistry, containerSource, targetRegistry, containerTarget, CopyUtils.CopyOptions.deep()); assertTrue(targetRegistry.exists(containerTarget)); diff --git a/src/test/java/land/oras/HarborS3ITCase.java b/src/test/java/land/oras/HarborS3ITCase.java index a181e15f..f7dfc451 100644 --- a/src/test/java/land/oras/HarborS3ITCase.java +++ b/src/test/java/land/oras/HarborS3ITCase.java @@ -178,20 +178,17 @@ void shouldPushJenkinsLibArtifact() { void shouldPushJenkinsScriptArtifact() { // language=groovy - String jenkinsfile = - """ - node { - stage('Build') { - echo 'Building...' - } - stage('Test') { - echo 'Testing...' - } - stage('Deploy') { - echo 'Deploying...' - } - } - """; + String jenkinsfile = "node {\n" + + " stage('Build') {\n" + + " echo 'Building...'\n" + + " }\n" + + " stage('Test') {\n" + + " echo 'Testing...'\n" + + " }\n" + + " stage('Deploy') {\n" + + " echo 'Deploying...'\n" + + " }\n" + + "}\n"; // The compressed manifests Path image = Paths.get("src/test/resources/img").resolve("jenkins.png"); diff --git a/src/test/java/land/oras/JFrogArtifactoryITCase.java b/src/test/java/land/oras/JFrogArtifactoryITCase.java index 9cf63f9e..8d53f068 100644 --- a/src/test/java/land/oras/JFrogArtifactoryITCase.java +++ b/src/test/java/land/oras/JFrogArtifactoryITCase.java @@ -74,7 +74,7 @@ void shouldCopyTagToInternalRegistry() { ContainerRef containerSource = ContainerRef.parse("releases-docker.jfrog.io/jfrog/jfrog-cli-v2-jf"); ContainerRef containerTarget = - ContainerRef.parse("%s/jfrog/jfrog-cli-v2-jf".formatted(unsecureRegistry.getRegistry())); + ContainerRef.parse(String.format("%s/jfrog/jfrog-cli-v2-jf", unsecureRegistry.getRegistry())); CopyUtils.copy(sourceRegistry, containerSource, targetRegistry, containerTarget, true); assertTrue(targetRegistry.exists(containerTarget)); diff --git a/src/test/java/land/oras/LayerTest.java b/src/test/java/land/oras/LayerTest.java index 76e33f83..25b2f510 100644 --- a/src/test/java/land/oras/LayerTest.java +++ b/src/test/java/land/oras/LayerTest.java @@ -77,14 +77,11 @@ void shouldHaveEmptyLayer() { @Test void shouldReadNullAnnotations() { - String json = - """ - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "digest": "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", - "size": 32654 - } - """; + String json = " {\n" + + " \"mediaType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n" + + " \"digest\": \"sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890\",\n" + + " \"size\": 32654\n" + + " }\n"; Layer layer = Layer.fromJson(json); assertEquals("application/vnd.oci.image.layer.v1.tar+gzip", layer.getMediaType()); assertEquals("sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", layer.getDigest()); @@ -94,14 +91,11 @@ void shouldReadNullAnnotations() { @Test void shouldReadBlobData() { - String json = - """ - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "digest": "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", - "data": "e30=" - } - """; + String json = " {\n" + + " \"mediaType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n" + + " \"digest\": \"sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890\",\n" + + " \"data\": \"e30=\"\n" + + " }\n"; Layer layer = Layer.fromJson(json); assertEquals("application/vnd.oci.image.layer.v1.tar+gzip", layer.getMediaType()); assertEquals("sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", layer.getDigest()); @@ -139,15 +133,13 @@ void testToString() { } private String emptyLayer() { - return """ - { - "mediaType": "application/vnd.oci.empty.v1+json", - "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", - "size": 2, - "data": "e30=", - "annotations": {} - } - """; + return " {\n" + + " \"mediaType\": \"application/vnd.oci.empty.v1+json\",\n" + + " \"digest\": \"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a\",\n" + + " \"size\": 2,\n" + + " \"data\": \"e30=\",\n" + + " \"annotations\": {}\n" + + " }\n"; } /** @@ -155,14 +147,11 @@ private String emptyLayer() { * @return The manifest */ private String sampleLayer() { - return Layer.fromJson( - """ - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "digest": "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", - "size": 32654 - } - """) + return Layer.fromJson(" {\n" + + " \"mediaType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n" + + " \"digest\": \"sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890\",\n" + + " \"size\": 32654\n" + + " }\n") .toJson(); } } diff --git a/src/test/java/land/oras/LayoutRefTest.java b/src/test/java/land/oras/LayoutRefTest.java index cb825767..163edc85 100644 --- a/src/test/java/land/oras/LayoutRefTest.java +++ b/src/test/java/land/oras/LayoutRefTest.java @@ -44,7 +44,7 @@ class LayoutRefTest { @Test void shouldParseLayoutWithAllParts() { String ociLayout = tempDir.resolve("foo").toString(); - LayoutRef layoutRef = LayoutRef.parse("%s:v1".formatted(ociLayout)); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s:v1", ociLayout)); assertEquals("v1", layoutRef.getTag()); assertEquals(ociLayout, layoutRef.getFolder().toString()); assertEquals(ociLayout, layoutRef.getRepository()); @@ -55,7 +55,7 @@ void shouldParseLayoutWithAllParts() { void shouldParseLayoutWithDigest() { String ociLayout = tempDir.resolve("foo").toString(); LayoutRef layoutRef = LayoutRef.parse( - "%s@sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824".formatted(ociLayout)); + String.format("%s@sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", ociLayout)); assertEquals("sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", layoutRef.getTag()); assertEquals(ociLayout, layoutRef.getFolder().toString()); assertEquals(ociLayout, layoutRef.getRepository()); diff --git a/src/test/java/land/oras/ManifestDescriptorTest.java b/src/test/java/land/oras/ManifestDescriptorTest.java index 92d0b611..71bc3d5b 100644 --- a/src/test/java/land/oras/ManifestDescriptorTest.java +++ b/src/test/java/land/oras/ManifestDescriptorTest.java @@ -86,21 +86,19 @@ void testToString() { private String descriptor() { // language=json - return """ - { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "digest": "sha256:09c8ec8bf0d43a250ba7fed2eb6f242935b2987be5ed921ee06c93008558f980", - "size": 838, - "annotations": { - "com.docker.official-images.bashbrew.arch": "riscv64", - "vnd.docker.reference.digest": "sha256:1de5eb4a9a6735adb46b2c9c88674c0cfba3444dd4ac2341b3babf1261700529", - "vnd.docker.reference.type": "attestation-manifest" - }, - "platform": { - "architecture": "unknown", - "os": "unknown" - } - } - """; + return "{\n" + + " \"mediaType\": \"application/vnd.oci.image.manifest.v1+json\",\n" + + " \"digest\": \"sha256:09c8ec8bf0d43a250ba7fed2eb6f242935b2987be5ed921ee06c93008558f980\",\n" + + " \"size\": 838,\n" + + " \"annotations\": {\n" + + " \"com.docker.official-images.bashbrew.arch\": \"riscv64\",\n" + + " \"vnd.docker.reference.digest\": \"sha256:1de5eb4a9a6735adb46b2c9c88674c0cfba3444dd4ac2341b3babf1261700529\",\n" + + " \"vnd.docker.reference.type\": \"attestation-manifest\"\n" + + " },\n" + + " \"platform\": {\n" + + " \"architecture\": \"unknown\",\n" + + " \"os\": \"unknown\"\n" + + " }\n" + + "}\n"; } } diff --git a/src/test/java/land/oras/ManifestTest.java b/src/test/java/land/oras/ManifestTest.java index 09d29651..f1888579 100644 --- a/src/test/java/land/oras/ManifestTest.java +++ b/src/test/java/land/oras/ManifestTest.java @@ -139,20 +139,18 @@ void shouldGetArtifactTest() { } private String emptyManifest() { - return """ - { - "schemaVersion": 2, - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "config": { - "mediaType": "application/vnd.oci.empty.v1+json", - "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", - "size": 2, - "data": "e30=" - }, - "layers": [], - "annotations": {} - } - """; + return "{\n" + + " \"schemaVersion\": 2,\n" + + " \"mediaType\": \"application/vnd.oci.image.manifest.v1+json\",\n" + + " \"config\": {\n" + + " \"mediaType\": \"application/vnd.oci.empty.v1+json\",\n" + + " \"digest\": \"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a\",\n" + + " \"size\": 2,\n" + + " \"data\": \"e30=\"\n" + + " },\n" + + " \"layers\": [],\n" + + " \"annotations\": {}\n" + + "}\n"; } /** @@ -161,28 +159,26 @@ private String emptyManifest() { */ private String sampleManifest() { // language=JSON - return """ - { - "schemaVersion": 2, - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "config": { - "mediaType": "application/vnd.oci.image.config.v1+json", - "digest": "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", - "size": 7023 - }, - "layers": [ - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "digest": "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", - "size": 32654 - }, - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "digest": "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "size": 1048576 - } - ] - } - """; + return " {\n" + + " \"schemaVersion\": 2,\n" + + " \"mediaType\": \"application/vnd.oci.image.manifest.v1+json\",\n" + + " \"config\": {\n" + + " \"mediaType\": \"application/vnd.oci.image.config.v1+json\",\n" + + " \"digest\": \"sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890\",\n" + + " \"size\": 7023\n" + + " },\n" + + " \"layers\": [\n" + + " {\n" + + " \"mediaType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n" + + " \"digest\": \"sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890\",\n" + + " \"size\": 32654\n" + + " },\n" + + " {\n" + + " \"mediaType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n" + + " \"digest\": \"sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef\",\n" + + " \"size\": 1048576\n" + + " }\n" + + " ]\n" + + " }\n"; } } diff --git a/src/test/java/land/oras/OCILayoutTest.java b/src/test/java/land/oras/OCILayoutTest.java index 849b0673..b5af69a9 100644 --- a/src/test/java/land/oras/OCILayoutTest.java +++ b/src/test/java/land/oras/OCILayoutTest.java @@ -65,7 +65,7 @@ class OCILayoutTest { @Test void shouldPushEmptyManifest() { Path path = layoutPath.resolve("shouldPushManifest"); - LayoutRef layoutRef = LayoutRef.parse("%s".formatted(path.toString())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s", path.toString())); OCILayout ociLayout = OCILayout.Builder.builder().defaults(path).build(); Manifest manifest = Manifest.empty().withConfig(Config.empty()); manifest = ociLayout.pushManifest(layoutRef, manifest); @@ -110,7 +110,7 @@ void shouldPushEmptyManifest() { @Test void shouldPushIndex() { Path path = layoutPath.resolve("shouldPushIndex"); - LayoutRef layoutRef = LayoutRef.parse("%s".formatted(path.toString())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s", path.toString())); OCILayout ociLayout = OCILayout.Builder.builder().defaults(path).build(); Index index = Index.fromManifests(List.of(Manifest.empty().getDescriptor())); index = ociLayout.pushIndex(layoutRef, index); @@ -148,7 +148,7 @@ void shouldPushSignedLayoutAndPullItAndValidateSignature(@TempDir Path homeDir) // A signed alpine OCI layout String imageDigest = "sha256:9e56ed4cb843f61658fcdb17d4205a87d5e217515f23831314b2173a776174d6"; - LayoutRef layoutRef = LayoutRef.parse("src/test/resources/oci/alpine-signed@%s".formatted(imageDigest)); + LayoutRef layoutRef = LayoutRef.parse(String.format("src/test/resources/oci/alpine-signed@%s", imageDigest)); OCILayout ociLayout = OCILayout.Builder.builder().defaults(layoutRef.getFolder()).build(); @@ -158,7 +158,7 @@ void shouldPushSignedLayoutAndPullItAndValidateSignature(@TempDir Path homeDir) .withInsecure(true) .build(); ContainerRef targetRef = - ContainerRef.parse("%s/library/alpine-signed:latest".formatted(registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/alpine-signed:latest", registry.getRegistry())); CopyUtils.copy(ociLayout, layoutRef, pushRegistry, targetRef, CopyUtils.CopyOptions.deep()); // Trust policy: accept only images carrying a valid Sigstore signature made by this key. @@ -168,17 +168,16 @@ void shouldPushSignedLayoutAndPullItAndValidateSignature(@TempDir Path homeDir) // language=json Files.writeString( policyPath, - """ - { - "default": [{"type": "reject"}], - "transports": { - "docker": { - "%s/library/alpine-signed": [{"type": "sigstoreSigned", "keyPath": "%s"}] - } - } - } - """ - .formatted(registry.getRegistry(), publicKeyPath.toAbsolutePath())); + String.format( + "{\n" + + " \"default\": [{\"type\": \"reject\"}],\n" + + " \"transports\": {\n" + + " \"docker\": {\n" + + " \"%s/library/alpine-signed\": [{\"type\": \"sigstoreSigned\", \"keyPath\": \"%s\"}]\n" + + " }\n" + + " }\n" + + "}\n", + registry.getRegistry(), publicKeyPath.toAbsolutePath())); ContainersPolicy policy = ContainersPolicy.newPolicy(policyPath); // Pull the manifest with the policy: the attached signature is fetched and verified. @@ -235,7 +234,7 @@ void shouldListRepositories() throws Exception { @Test void shouldPushConfig() throws IOException { Path path = layoutPath.resolve("shouldPushConfig"); - LayoutRef layoutRef = LayoutRef.parse("%s".formatted(path.toString())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s", path.toString())); OCILayout ociLayout = OCILayout.Builder.builder().defaults(path).build(); Config config = Config.empty(); ociLayout.pushConfig(layoutRef.withDigest(config.getDigest()), config); @@ -253,7 +252,7 @@ void shouldPushConfig() throws IOException { @Test void shouldPushConfigWithReference() throws IOException { Path path = layoutPath.resolve("shouldPushConfigWithReference"); - LayoutRef layoutRef = LayoutRef.parse("%s".formatted(path.toString())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s", path.toString())); OCILayout ociLayout = OCILayout.Builder.builder().defaults(path).build(); Path configFile = blobDir.resolve("config.txt"); Files.writeString(configFile, "hello"); @@ -275,7 +274,7 @@ void shouldPushConfigWithReference() throws IOException { @Test void shouldRejectTamperedBlobOnRead() throws IOException { Path path = layoutPath.resolve("tamperedBlob"); - LayoutRef layoutRef = LayoutRef.parse("%s".formatted(path.toString())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s", path.toString())); OCILayout ociLayout = OCILayout.Builder.builder().defaults(path).build(); // Push a known blob into the layout @@ -308,7 +307,7 @@ void shouldRejectTamperedBlobOnRead() throws IOException { @Test void shouldReadUntamperedBlobAfterIntegrityCheck() throws IOException { Path path = layoutPath.resolve("untamperedBlob"); - LayoutRef layoutRef = LayoutRef.parse("%s".formatted(path.toString())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s", path.toString())); OCILayout ociLayout = OCILayout.Builder.builder().defaults(path).build(); Path blobFile = blobDir.resolve("intact.txt"); @@ -333,7 +332,7 @@ void shouldRejectTamperedLayerOnPullArtifact() throws IOException { Path artifactPath = blobDir.resolve("artifact.txt"); Files.writeString(artifactPath, "artifact-content"); - LayoutRef layoutRef = LayoutRef.parse("%s:latest".formatted(ociLayoutPath.toString())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s:latest", ociLayoutPath.toString())); OCILayout ociLayout = OCILayout.Builder.builder().defaults(ociLayoutPath).build(); Annotations annotations = Annotations.ofManifest(Map.of(Const.ANNOTATION_CREATED, Const.currentTimestamp())); @@ -400,7 +399,7 @@ void verifyBlobDigestIgnoresMissingBlob() { @Test void shouldPushIndexWithTag() { Path path = layoutPath.resolve("shouldPushIndexWithTag"); - LayoutRef layoutRef = LayoutRef.parse("%s".formatted(path.toString())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s", path.toString())); OCILayout ociLayout = OCILayout.Builder.builder().defaults(path).build(); Index index = Index.fromManifests(List.of(Manifest.empty().getDescriptor())); index = ociLayout.pushIndex(layoutRef.withTag("latest"), index); @@ -423,7 +422,7 @@ void shouldPushIndexWithTag() { void shouldPushManifestFromFile() { Path path = layoutPath.resolve("shouldPushManifetFromFile"); - LayoutRef layoutRef = LayoutRef.parse("%s".formatted(path.toString())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s", path.toString())); OCILayout ociLayout = OCILayout.Builder.builder().defaults(path).build(); Manifest manifest = Manifest.fromPath( @@ -461,7 +460,7 @@ void shouldPushManifestFromFile() { @Test void shouldPushEmptyManifestWithRef() { Path path = layoutPath.resolve("shouldPushManifest"); - LayoutRef layoutRef = LayoutRef.parse("%s:latest".formatted(path.toString())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s:latest", path.toString())); OCILayout ociLayout = OCILayout.Builder.builder().defaults(path).build(); Manifest manifest = Manifest.empty().withConfig(Config.empty()); manifest = ociLayout.pushManifest(layoutRef, manifest); @@ -531,7 +530,7 @@ void failToCreateLayoutIfFileExists() throws IOException { Path path = layoutPath.resolve("failToCreateLayoutIfFileExists"); Files.createFile(path); assertThrows(OrasException.class, () -> { - LayoutRef layoutRef = LayoutRef.parse("%s".formatted(path.toString())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s", path.toString())); OCILayout.Builder.builder().defaults(layoutRef.getFolder()).build(); }); } @@ -543,7 +542,7 @@ void shouldPushToOciLayoutWithoutTag() throws IOException { Path artifactPath = blobDir.resolve("shouldPushToOciLayoutWithoutTag.txt"); Files.writeString(artifactPath, "hi"); - LayoutRef layoutRef = LayoutRef.parse("%s".formatted(ociLayoutPath.toString())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s", ociLayoutPath.toString())); OCILayout ociLayout = OCILayout.Builder.builder().defaults(ociLayoutPath).build(); @@ -640,7 +639,7 @@ void shouldPushToOciLayoutWithTag() throws IOException { Path artifactPath = blobDir.resolve("shouldPushToOciLayoutWithTag.txt"); Files.writeString(artifactPath, "hi"); - LayoutRef layoutRef = LayoutRef.parse("%s:latest".formatted(ociLayoutPath.toString())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s:latest", ociLayoutPath.toString())); OCILayout ociLayout = OCILayout.Builder.builder().defaults(ociLayoutPath).build(); @@ -916,7 +915,7 @@ void cannotPushBlobFromPathWithoutTagOrDigest() throws IOException { void cannotPushWithInvalidDigest() { Path invalidBlobPushDir = layoutPath.resolve("cannotPushWithInvalidDigest"); - LayoutRef wrongDigest1 = LayoutRef.parse("%s@sha234:1234".formatted(invalidBlobPushDir.toString())); + LayoutRef wrongDigest1 = LayoutRef.parse(String.format("%s@sha234:1234", invalidBlobPushDir.toString())); OCILayout ociLayout = OCILayout.Builder.builder().defaults(invalidBlobPushDir).build(); @@ -925,7 +924,7 @@ void cannotPushWithInvalidDigest() { ociLayout.pushBlob(wrongDigest1, "hi".getBytes(StandardCharsets.UTF_8)); }); - LayoutRef wrongDigest2 = LayoutRef.parse("%s@sha256:1234".formatted(invalidBlobPushDir.toString())); + LayoutRef wrongDigest2 = LayoutRef.parse(String.format("%s@sha256:1234", invalidBlobPushDir.toString())); // Push more blobs assertThrows(OrasException.class, () -> { @@ -945,7 +944,7 @@ void testShouldCopyArtifactFromRegistryIntoOciLayout() throws IOException { LayoutRef layoutRef = LayoutRef.of(ociLayout); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-oci-layout".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-oci-layout", this.registry.getRegistry())); Path file1 = blobDir.resolve("artifact-oci-layout.txt"); Path file2 = blobDir.resolve("artifact-recursive-oci-attached.txt"); Files.writeString(file1, "artifact-oci-layout"); @@ -985,10 +984,10 @@ void testShouldCopyIndexWithPlatformFilterFromRegistryIntoOciLayout() throws IOE Path ociLayoutPath = layoutPath.resolve("testShouldCopyIndexWithPlatformFilterFromRegistryIntoOciLayout"); OCILayout ociLayout = OCILayout.Builder.builder().defaults(ociLayoutPath).build(); - LayoutRef layoutRef = LayoutRef.parse("%s:latest".formatted(ociLayoutPath.toString())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s:latest", ociLayoutPath.toString())); ContainerRef containerRef = - ContainerRef.parse("%s/library/platform-filter-source".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/platform-filter-source", this.registry.getRegistry())); // Push two manifests with different content, one per platform Path fileAmd64 = blobDir.resolve("platform-filter-amd64.txt"); @@ -1062,7 +1061,7 @@ void testShouldCopyFromOciLayoutIntoOciLayoutRecursive() throws IOException { // Target Path ociLayoutPath = layoutPath.resolve("testShouldCopyFromOciLayoutIntoOciLayoutRecursive"); - LayoutRef targetRef = LayoutRef.parse("%s".formatted(ociLayoutPath.toString())); + LayoutRef targetRef = LayoutRef.parse(String.format("%s", ociLayoutPath.toString())); OCILayout target = OCILayout.builder().defaults(targetRef.getFolder()).build(); // Copy to oci layout @@ -1092,7 +1091,7 @@ void testShouldCopyFromOciLayoutIntoOciLayoutNonRecursive() throws IOException { // Target Path ociLayoutPath = layoutPath.resolve("testShouldCopyFromOciLayoutIntoOciLayoutNonRecursive"); - LayoutRef targetRef = LayoutRef.parse("%s".formatted(ociLayoutPath.toString())); + LayoutRef targetRef = LayoutRef.parse(String.format("%s", ociLayoutPath.toString())); OCILayout target = OCILayout.builder().defaults(targetRef.getFolder()).build(); // Copy to oci layout @@ -1122,10 +1121,10 @@ void testShouldCopyRecursivelyArtifactFromRegistryIntoOciLayout() throws IOExcep .build(); OCILayout ociLayout = OCILayout.Builder.builder().defaults(layoutPath).build(); - LayoutRef layoutRef = LayoutRef.parse("%s".formatted(ociLayout.getPath())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s", ociLayout.getPath())); - ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-recursive-oci-layout".formatted(this.registry.getRegistry())); + ContainerRef containerRef = ContainerRef.parse( + String.format("%s/library/artifact-recursive-oci-layout", this.registry.getRegistry())); Path file1 = blobDir.resolve("artifact-recursive-oci-layout.txt"); Path file2 = blobDir.resolve("artifact-recursive-oci-attached.txt"); Path file3 = blobDir.resolve("artifact-recursive-oci-attached2.txt"); @@ -1173,10 +1172,10 @@ void testShouldCopyImageIntoOciLayoutWithoutIndexAndTag() { .build(); OCILayout ociLayout = OCILayout.Builder.builder().defaults(layoutPath).build(); - LayoutRef layoutRef = LayoutRef.parse("%s:the-tag".formatted(ociLayout.getPath())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s:the-tag", ociLayout.getPath())); ContainerRef containerRef = - ContainerRef.parse("%s/library/image-no-index".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/image-no-index", this.registry.getRegistry())); Layer layer1 = registry.pushBlob(containerRef, Layer.empty().getDataBytes()); Layer layer2 = registry.pushBlob(containerRef, "foobar".getBytes()); @@ -1251,10 +1250,10 @@ void testShouldCopyImageIntoOciLayoutWithIndex() { OCILayout ociLayout = OCILayout.Builder.builder().defaults(layoutPathIndex).build(); - LayoutRef layoutRef = LayoutRef.parse("%s:latest".formatted(ociLayout.getPath())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s:latest", ociLayout.getPath())); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-image-pull".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-image-pull", this.registry.getRegistry())); Layer layer1 = registry.pushBlob(containerRef, Layer.empty().getDataBytes()); Layer layer2 = registry.pushBlob(containerRef, "foobar".getBytes()); @@ -1323,10 +1322,10 @@ void testShouldCopyIntoOciLayoutWithBlobConfig() throws IOException { .build(); OCILayout ociLayout = OCILayout.Builder.builder().defaults(layoutPath).build(); - LayoutRef layoutRef = LayoutRef.parse("%s".formatted(ociLayout.getPath())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s", ociLayout.getPath())); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-oci-layout".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-oci-layout", this.registry.getRegistry())); Path file1 = blobDir.resolve("artifact-oci-layout.txt"); Files.writeString(file1, "artifact-oci-layout"); @@ -1431,7 +1430,7 @@ void pullArtifactShouldRejectInvalidTitleAnnotation() throws IOException { Path artifactFile = blobDir.resolve("safe.txt"); Files.writeString(artifactFile, "safe content"); - LayoutRef layoutRef = LayoutRef.parse("%s:latest".formatted(ociLayoutPath.toString())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s:latest", ociLayoutPath.toString())); OCILayout ociLayout = OCILayout.Builder.builder().defaults(ociLayoutPath).build(); ociLayout.pushArtifact( @@ -1531,7 +1530,7 @@ void shouldPushAndPullArtifactViaTagInTarBackedLayout() throws IOException { Path artifactFile = blobDir.resolve("hello-tar.txt"); Files.writeString(artifactFile, "hello from tar"); - LayoutRef ref = LayoutRef.parse("%s:latest".formatted(tarFile.toString())); + LayoutRef ref = LayoutRef.parse(String.format("%s:latest", tarFile.toString())); OCILayout ociLayout = OCILayout.Builder.builder().defaults(tarFile).build(); // Act: push @@ -1543,7 +1542,7 @@ void shouldPushAndPullArtifactViaTagInTarBackedLayout() throws IOException { // Re-open the layout from the same tar (simulate a second process) OCILayout reopened = OCILayout.Builder.builder().defaults(tarFile).build(); - LayoutRef reopenedRef = LayoutRef.parse("%s:latest".formatted(tarFile.toString())); + LayoutRef reopenedRef = LayoutRef.parse(String.format("%s:latest", tarFile.toString())); // Pull the artifact Path pullDir = extractDir.resolve("tar-pull-out"); @@ -1561,7 +1560,7 @@ void shouldListTagsFromTarBackedLayout() throws IOException { // Open the pre-built artifact.tar fixture Path tarFile = Path.of("src/test/resources/oci/artifact.tar"); OCILayout ociLayout = OCILayout.Builder.builder().defaults(tarFile).build(); - LayoutRef ref = LayoutRef.parse("%s:latest".formatted(tarFile.toString())); + LayoutRef ref = LayoutRef.parse(String.format("%s:latest", tarFile.toString())); Tags tags = ociLayout.getTags(ref); assertEquals(1, tags.tags().size()); @@ -1573,7 +1572,7 @@ void shouldGetReferrersFromTarBackedLayout() throws IOException { // Open the pre-built subject.tar fixture (has one referrer) Path tarFile = Path.of("src/test/resources/oci/subject.tar"); OCILayout ociLayout = OCILayout.Builder.builder().defaults(tarFile).build(); - LayoutRef ref = LayoutRef.parse("%s:latest".formatted(tarFile.toString())); + LayoutRef ref = LayoutRef.parse(String.format("%s:latest", tarFile.toString())); Referrers referrers = ociLayout.getReferrers(ref, null); assertEquals(1, referrers.getManifests().size()); @@ -1587,7 +1586,7 @@ void shouldPullArtifactFromTarFixture() throws IOException { // Open the pre-built artifact.tar fixture Path tarFile = Path.of("src/test/resources/oci/artifact.tar"); OCILayout ociLayout = OCILayout.Builder.builder().defaults(tarFile).build(); - LayoutRef ref = LayoutRef.parse("%s:latest".formatted(tarFile.toString())); + LayoutRef ref = LayoutRef.parse(String.format("%s:latest", tarFile.toString())); Path pullDir = extractDir.resolve("tar-fixture-pull"); Files.createDirectories(pullDir); @@ -1601,12 +1600,12 @@ void shouldPullArtifactFromTarFixture() throws IOException { void shouldReopenExistingTarAndPushAdditionalManifest() throws IOException { // First session: create a tar-backed layout and push one manifest Path tarFile = layoutPath.resolve("reopen-test.tar"); - LayoutRef ref1 = LayoutRef.parse("%s:v1".formatted(tarFile.toString())); + LayoutRef ref1 = LayoutRef.parse(String.format("%s:v1", tarFile.toString())); OCILayout session1 = OCILayout.Builder.builder().defaults(tarFile).build(); session1.pushManifest(ref1, Manifest.empty().withConfig(Config.empty())); // Second session: reopen the same tar and push another manifest - LayoutRef ref2 = LayoutRef.parse("%s:v2".formatted(tarFile.toString())); + LayoutRef ref2 = LayoutRef.parse(String.format("%s:v2", tarFile.toString())); OCILayout session2 = OCILayout.Builder.builder().defaults(tarFile).build(); Manifest m2 = Manifest.empty().withConfig(Config.empty()).withAnnotations(Map.of("version", "2")); session2.pushManifest(ref2, m2); @@ -1642,7 +1641,7 @@ void testShouldCopyArtifactFromRegistryIntoTarBackedOciLayout() throws IOExcepti LayoutRef layoutRef = LayoutRef.of(ociLayout); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-oci-layout-tar".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-oci-layout-tar", this.registry.getRegistry())); Path file1 = blobDir.resolve("artifact-oci-layout-tar.txt"); Files.writeString(file1, "artifact-oci-layout-tar"); @@ -1676,10 +1675,10 @@ void testShouldCopyArtifactRecursivelyFromRegistryIntoTarBackedOciLayout() throw Path tarFile = layoutPath.resolve("copy-deep.tar"); OCILayout ociLayout = OCILayout.Builder.builder().defaults(tarFile).build(); - LayoutRef layoutRef = LayoutRef.parse("%s".formatted(ociLayout.getPath())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s", ociLayout.getPath())); ContainerRef containerRef = ContainerRef.parse( - "%s/library/artifact-recursive-oci-layout-tar".formatted(this.registry.getRegistry())); + String.format("%s/library/artifact-recursive-oci-layout-tar", this.registry.getRegistry())); Path file1 = blobDir.resolve("artifact-recursive-oci-layout-tar.txt"); Path file2 = blobDir.resolve("artifact-recursive-oci-attached-tar.txt"); Path file3 = blobDir.resolve("artifact-recursive-oci-attached2-tar.txt"); @@ -1723,7 +1722,7 @@ void shouldGarbageCollectReturnEmptyWhenNoBlobsAreOrphaned() throws IOException Path artifactFile = blobDir.resolve("gc-no-orphan.txt"); Files.writeString(artifactFile, "no-orphan"); - LayoutRef layoutRef = LayoutRef.parse("%s:latest".formatted(ociLayoutPath.toString())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s:latest", ociLayoutPath.toString())); OCILayout ociLayout = OCILayout.Builder.builder().defaults(ociLayoutPath).build(); ociLayout.pushArtifact( @@ -1747,7 +1746,7 @@ void shouldGarbageCollectRemoveOrphanedBlob() throws IOException { Path orphanFile = blobDir.resolve("gc-orphan-extra.txt"); Files.writeString(orphanFile, "orphaned-blob-content"); - LayoutRef layoutRef = LayoutRef.parse("%s:latest".formatted(ociLayoutPath.toString())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s:latest", ociLayoutPath.toString())); OCILayout ociLayout = OCILayout.Builder.builder().defaults(ociLayoutPath).build(); ociLayout.pushArtifact( @@ -1782,7 +1781,7 @@ void shouldGarbageCollectMultipleOrphanedBlobs() throws IOException { Path artifactFile = blobDir.resolve("gc-multi-orphan.txt"); Files.writeString(artifactFile, "referenced-multi"); - LayoutRef layoutRef = LayoutRef.parse("%s:latest".formatted(ociLayoutPath.toString())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s:latest", ociLayoutPath.toString())); OCILayout ociLayout = OCILayout.Builder.builder().defaults(ociLayoutPath).build(); ociLayout.pushArtifact( @@ -1826,8 +1825,8 @@ void shouldGarbageCollectKeepAllBlobsAfterMultipleManifests() throws IOException OCILayout ociLayout = OCILayout.Builder.builder().defaults(ociLayoutPath).build(); - LayoutRef ref1 = LayoutRef.parse("%s:v1".formatted(ociLayoutPath.toString())); - LayoutRef ref2 = LayoutRef.parse("%s:v2".formatted(ociLayoutPath.toString())); + LayoutRef ref1 = LayoutRef.parse(String.format("%s:v1", ociLayoutPath.toString())); + LayoutRef ref2 = LayoutRef.parse(String.format("%s:v2", ociLayoutPath.toString())); ociLayout.pushArtifact(ref1, ArtifactType.from("foo/bar"), Annotations.empty(), LocalPath.of(file1)); ociLayout.pushArtifact(ref2, ArtifactType.from("foo/bar"), Annotations.empty(), LocalPath.of(file2)); @@ -1849,7 +1848,7 @@ void shouldGarbageCollectKeepReferrerBlobsWhenCopiedDeep() throws IOException { Files.writeString(mainFile, "main-artifact"); Files.writeString(attachFile, "attached-artifact"); - LayoutRef layoutRef = LayoutRef.parse("%s:latest".formatted(ociLayoutPath.toString())); + LayoutRef layoutRef = LayoutRef.parse(String.format("%s:latest", ociLayoutPath.toString())); OCILayout ociLayout = OCILayout.Builder.builder().defaults(ociLayoutPath).build(); @@ -1901,8 +1900,8 @@ void shouldGarbageCollectWithNestedIndex() throws IOException { OCILayout.Builder.builder().defaults(ociLayoutPath).build(); // Push two independent manifests (without a top-level tag so they get digest-only entries) - LayoutRef ref1 = LayoutRef.parse("%s".formatted(ociLayoutPath.toString())); - LayoutRef ref2 = LayoutRef.parse("%s".formatted(ociLayoutPath.toString())); + LayoutRef ref1 = LayoutRef.parse(String.format("%s", ociLayoutPath.toString())); + LayoutRef ref2 = LayoutRef.parse(String.format("%s", ociLayoutPath.toString())); Manifest manifest1 = ociLayout.pushArtifact( ref1, ArtifactType.from("foo/bar"), Annotations.empty(), LocalPath.of(file1, "text/plain")); Manifest manifest2 = ociLayout.pushArtifact( @@ -1912,7 +1911,7 @@ void shouldGarbageCollectWithNestedIndex() throws IOException { assertNotNull(manifest1.getDescriptor(), "Manifest 1 descriptor should not be null"); assertNotNull(manifest2.getDescriptor(), "Manifest 2 descriptor should not be null"); Index nestedIndex = Index.fromManifests(List.of(manifest1.getDescriptor(), manifest2.getDescriptor())); - LayoutRef indexRef = LayoutRef.parse("%s:multi".formatted(ociLayoutPath.toString())); + LayoutRef indexRef = LayoutRef.parse(String.format("%s:multi", ociLayoutPath.toString())); Index pushedIndex = ociLayout.pushIndex(indexRef, nestedIndex); // Collect the digests that must survive GC diff --git a/src/test/java/land/oras/PlatformTest.java b/src/test/java/land/oras/PlatformTest.java index 52455d5a..1dd3f86b 100644 --- a/src/test/java/land/oras/PlatformTest.java +++ b/src/test/java/land/oras/PlatformTest.java @@ -112,26 +112,14 @@ void shouldTestUnknownPlatform() { @Test void shouldReadFromJson() { // language=json - String json = - """ - { - "architecture": "amd64", - "os": "linux" - } - """; + String json = "{\n" + " \"architecture\": \"amd64\",\n" + " \"os\": \"linux\"\n" + "}\n"; Platform platform = JsonUtils.fromJson(json, Platform.class); assertEquals("amd64", platform.architecture()); assertEquals("linux", platform.os()); assertNull(platform.variant()); assertEquals(Platform.linuxAmd64(), platform); - json = - """ - { - "architecture": "unknown", - "os": "unknown" - } - """; + json = "{\n" + " \"architecture\": \"unknown\",\n" + " \"os\": \"unknown\"\n" + "}\n"; platform = JsonUtils.fromJson(json, Platform.class); assertEquals(Platform.unknown(), platform); } @@ -139,17 +127,14 @@ void shouldReadFromJson() { @Test void shouldReadFromJsonWithOptionalValues() { // language=json - String json = - """ - { - "architecture": "amd64", - "variant": "v8", - "os.features": ["linux-gnu"], - "os.version": "1.0", - "features": ["sse4", "avx2"], - "os": "linux" - } - """; + String json = "{\n" + + " \"architecture\": \"amd64\",\n" + + " \"variant\": \"v8\",\n" + + " \"os.features\": [\"linux-gnu\"],\n" + + " \"os.version\": \"1.0\",\n" + + " \"features\": [\"sse4\", \"avx2\"],\n" + + " \"os\": \"linux\"\n" + + "}\n"; Platform platform = JsonUtils.fromJson(json, Platform.class); assertEquals("amd64", platform.architecture()); assertEquals("linux", platform.os()); @@ -164,13 +149,7 @@ void shouldReadFromJsonWithOptionalValues() { .withVariant("v8"), platform); - json = - """ - { - "architecture": "unknown", - "os": "unknown" - } - """; + json = "{\n" + " \"architecture\": \"unknown\",\n" + " \"os\": \"unknown\"\n" + "}\n"; platform = JsonUtils.fromJson(json, Platform.class); assertEquals(Platform.unknown(), platform); } diff --git a/src/test/java/land/oras/PublicECRITCase.java b/src/test/java/land/oras/PublicECRITCase.java index bbc29b6a..f315d541 100644 --- a/src/test/java/land/oras/PublicECRITCase.java +++ b/src/test/java/land/oras/PublicECRITCase.java @@ -41,9 +41,7 @@ class PublicECRITCase { void shouldDetermineEffectiveRegistryWithUnqualifiedSettings() throws Exception { // language=toml - String config = """ - unqualified-search-registries = ["public.ecr.aws"] - """; + String config = "unqualified-search-registries = [\"public.ecr.aws\"]\n"; TestUtils.createRegistriesConfFile(homeDir, config); @@ -60,12 +58,9 @@ void shouldDetermineEffectiveRegistryWithUnqualifiedSettings() throws Exception void shouldRewriteDockerIOToPublicECR() throws Exception { // language=toml - String config = - """ - [[registry]] - prefix = "docker.io/library" - location = "public.ecr.aws/docker/library" - """; + String config = "[[registry]]\n" + + "prefix = \"docker.io/library\"\n" + + "location = \"public.ecr.aws/docker/library\"\n"; TestUtils.createRegistriesConfFile(homeDir, config); diff --git a/src/test/java/land/oras/ReferrersTest.java b/src/test/java/land/oras/ReferrersTest.java index 105bb05c..4fe30f0c 100644 --- a/src/test/java/land/oras/ReferrersTest.java +++ b/src/test/java/land/oras/ReferrersTest.java @@ -33,22 +33,20 @@ class ReferrersTest { private String descriptor() { // language=json - return """ - { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "digest": "sha256:09c8ec8bf0d43a250ba7fed2eb6f242935b2987be5ed921ee06c93008558f980", - "size": 838, - "annotations": { - "com.docker.official-images.bashbrew.arch": "riscv64", - "vnd.docker.reference.digest": "sha256:1de5eb4a9a6735adb46b2c9c88674c0cfba3444dd4ac2341b3babf1261700529", - "vnd.docker.reference.type": "attestation-manifest" - }, - "platform": { - "architecture": "unknown", - "os": "unknown" - } - } - """; + return "{\n" + + " \"mediaType\": \"application/vnd.oci.image.manifest.v1+json\",\n" + + " \"digest\": \"sha256:09c8ec8bf0d43a250ba7fed2eb6f242935b2987be5ed921ee06c93008558f980\",\n" + + " \"size\": 838,\n" + + " \"annotations\": {\n" + + " \"com.docker.official-images.bashbrew.arch\": \"riscv64\",\n" + + " \"vnd.docker.reference.digest\": \"sha256:1de5eb4a9a6735adb46b2c9c88674c0cfba3444dd4ac2341b3babf1261700529\",\n" + + " \"vnd.docker.reference.type\": \"attestation-manifest\"\n" + + " },\n" + + " \"platform\": {\n" + + " \"architecture\": \"unknown\",\n" + + " \"os\": \"unknown\"\n" + + " }\n" + + "}\n"; } @Test diff --git a/src/test/java/land/oras/RegistryMirrorTest.java b/src/test/java/land/oras/RegistryMirrorTest.java index 2d1f8e55..ec69e488 100644 --- a/src/test/java/land/oras/RegistryMirrorTest.java +++ b/src/test/java/land/oras/RegistryMirrorTest.java @@ -70,21 +70,19 @@ void shouldFetchManifestViaMirrorWhenOriginalIsDown(@TempDir Path blobDir) throw // mirror 1: localhost:59999 (down, connection refused) // mirror 2: the running mirrorUp container // language=toml - String registriesConf = - """ - [[registry]] - prefix = "localhost:59998" - location = "localhost:59998" - - [[registry.mirror]] - location = "localhost:59999" - insecure = true - - [[registry.mirror]] - location = "%s" - insecure = true - """ - .formatted(mirrorRegistry); + String registriesConf = String.format( + "[[registry]]\n" + + "prefix = \"localhost:59998\"\n" + + "location = \"localhost:59998\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"localhost:59999\"\n" + + "insecure = true\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"%s\"\n" + + "insecure = true\n", + mirrorRegistry); TestUtils.createRegistriesConfFile(homeDir, registriesConf); @@ -107,21 +105,19 @@ void shouldPullArtifactViaMirrorWhenOriginalIsDown(@TempDir Path blobDir, @TempD setupRegistry.pushArtifact(mirrorArtifact, LocalPath.of(testFile)); // language=toml - String registriesConf = - """ - [[registry]] - prefix = "localhost:59998" - location = "localhost:59998" - - [[registry.mirror]] - location = "localhost:59999" - insecure = true - - [[registry.mirror]] - location = "%s" - insecure = true - """ - .formatted(mirrorRegistry); + String registriesConf = String.format( + "[[registry]]\n" + + "prefix = \"localhost:59998\"\n" + + "location = \"localhost:59998\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"localhost:59999\"\n" + + "insecure = true\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"%s\"\n" + + "insecure = true\n", + mirrorRegistry); TestUtils.createRegistriesConfFile(homeDir, registriesConf); @@ -147,17 +143,15 @@ void shouldFetchManifestViaInsecurePathPrefixedMirror(@TempDir Path blobDir) thr // Mirror location includes a path prefix → covers mirrorLocation.contains("/") branch. // language=toml - String registriesConf = - """ - [[registry]] - prefix = "localhost:59998" - location = "localhost:59998" - - [[registry.mirror]] - location = "%s/prefix" - insecure = true - """ - .formatted(mirrorRegistry); + String registriesConf = String.format( + "[[registry]]\n" + + "prefix = \"localhost:59998\"\n" + + "location = \"localhost:59998\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"%s/prefix\"\n" + + "insecure = true\n", + mirrorRegistry); TestUtils.createRegistriesConfFile(homeDir, registriesConf); @@ -182,21 +176,19 @@ void shouldFetchBlobViaMirrorWhenOriginalIsDown(@TempDir Path blobDir, @TempDir Layer layer = setupRegistry.pushBlob(mirrorRef, blobContent); // language=toml - String registriesConf = - """ - [[registry]] - prefix = "localhost:59998" - location = "localhost:59998" - - [[registry.mirror]] - location = "localhost:59999" - insecure = true - - [[registry.mirror]] - location = "%s" - insecure = true - """ - .formatted(mirrorRegistry); + String registriesConf = String.format( + "[[registry]]\n" + + "prefix = \"localhost:59998\"\n" + + "location = \"localhost:59998\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"localhost:59999\"\n" + + "insecure = true\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"%s\"\n" + + "insecure = true\n", + mirrorRegistry); TestUtils.createRegistriesConfFile(homeDir, registriesConf); @@ -241,21 +233,19 @@ void shouldFetchManifestViaUnqualifiedReference(@TempDir Path blobDir) throws Ex // Configure docker.io with a mirror — unqualified refs resolve to docker.io by default // language=toml - String registriesConf = - """ - short-name-mode = "disabled" - - unqualified-search-registries = ["docker.io"] - - [[registry]] - prefix = "docker.io" - location = "docker.io" - - [[registry.mirror]] - location = "%s" - insecure = true - """ - .formatted(mirrorRegistry); + String registriesConf = String.format( + "short-name-mode = \"disabled\"\n" + + "\n" + + "unqualified-search-registries = [\"docker.io\"]\n" + + "\n" + + "[[registry]]\n" + + "prefix = \"docker.io\"\n" + + "location = \"docker.io\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"%s\"\n" + + "insecure = true\n", + mirrorRegistry); TestUtils.createRegistriesConfFile(homeDir, registriesConf); @@ -286,20 +276,18 @@ void shouldResolveUnqualifiedRegistryViaFallbackWhenMirrorIsDown(@TempDir Path b // is actually configured as the search registry below. The mirror here is unreachable: resolution // must catch that failure and fall back to probing the search registry directly. // language=toml - String registriesConf = - """ - short-name-mode = "disabled" - - unqualified-search-registries = ["%s"] - - [[registry]] - prefix = "docker.io" - - [[registry.mirror]] - location = "localhost:59999" - insecure = true - """ - .formatted(searchRegistry); + String registriesConf = String.format( + "short-name-mode = \"disabled\"\n" + + "\n" + + "unqualified-search-registries = [\"%s\"]\n" + + "\n" + + "[[registry]]\n" + + "prefix = \"docker.io\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"localhost:59999\"\n" + + "insecure = true\n", + searchRegistry); TestUtils.createRegistriesConfFile(homeDir, registriesConf); @@ -323,20 +311,18 @@ void shouldThrowWhenMirrorIsDownAndSearchRegistryLacksArtifact() throws Exceptio String searchRegistry = mirrorUp.getRegistry(); // language=toml - String registriesConf = - """ - short-name-mode = "disabled" - - unqualified-search-registries = ["%s"] - - [[registry]] - prefix = "docker.io" - - [[registry.mirror]] - location = "localhost:59999" - insecure = true - """ - .formatted(searchRegistry); + String registriesConf = String.format( + "short-name-mode = \"disabled\"\n" + + "\n" + + "unqualified-search-registries = [\"%s\"]\n" + + "\n" + + "[[registry]]\n" + + "prefix = \"docker.io\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"localhost:59999\"\n" + + "insecure = true\n", + searchRegistry); TestUtils.createRegistriesConfFile(homeDir, registriesConf); @@ -349,7 +335,7 @@ void shouldThrowWhenMirrorIsDownAndSearchRegistryLacksArtifact() throws Exceptio e.getMessage() .startsWith("Container reference library/does-not-exist:v1 is unqualified" + " and cannot be found in any of the unqualified search registries"), - "Wrong exception message: got '%s'".formatted(e.getMessage())); + String.format("Wrong exception message: got '%s'", e.getMessage())); }); } @@ -365,18 +351,16 @@ void shouldSkipDigestOnlyMirrorWhenPullingByTag(@TempDir Path blobDir) throws Ex // Mirror configured as digest-only — a tag-based pull must NOT use it // language=toml - String registriesConf = - """ - [[registry]] - prefix = "localhost:59998" - location = "localhost:59998" - - [[registry.mirror]] - location = "%s" - insecure = true - pull-from-mirror = "digest-only" - """ - .formatted(mirrorRegistry); + String registriesConf = String.format( + "[[registry]]\n" + + "prefix = \"localhost:59998\"\n" + + "location = \"localhost:59998\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"%s\"\n" + + "insecure = true\n" + + "pull-from-mirror = \"digest-only\"\n", + mirrorRegistry); TestUtils.createRegistriesConfFile(homeDir, registriesConf); @@ -407,18 +391,16 @@ void shouldUseDigestOnlyMirrorWhenPullingByDigest(@TempDir Path blobDir) throws String digest = pushed.getDescriptor().getDigest(); // language=toml - String registriesConf = - """ - [[registry]] - prefix = "localhost:59998" - location = "localhost:59998" - - [[registry.mirror]] - location = "%s" - insecure = true - pull-from-mirror = "digest-only" - """ - .formatted(mirrorRegistry); + String registriesConf = String.format( + "[[registry]]\n" + + "prefix = \"localhost:59998\"\n" + + "location = \"localhost:59998\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"%s\"\n" + + "insecure = true\n" + + "pull-from-mirror = \"digest-only\"\n", + mirrorRegistry); TestUtils.createRegistriesConfFile(homeDir, registriesConf); @@ -447,18 +429,16 @@ void shouldApplyMirrorByDigestOnly(@TempDir Path blobDir) throws Exception { String digest = pushed.getDescriptor().getDigest(); // language=toml - String registriesConf = - """ - [[registry]] - prefix = "localhost:59998" - location = "localhost:59998" - mirror-by-digest-only = true - - [[registry.mirror]] - location = "%s" - insecure = true - """ - .formatted(mirrorRegistry); + String registriesConf = String.format( + "[[registry]]\n" + + "prefix = \"localhost:59998\"\n" + + "location = \"localhost:59998\"\n" + + "mirror-by-digest-only = true\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"%s\"\n" + + "insecure = true\n", + mirrorRegistry); TestUtils.createRegistriesConfFile(homeDir, registriesConf); diff --git a/src/test/java/land/oras/RegistryTest.java b/src/test/java/land/oras/RegistryTest.java index e180a5e2..0a394254 100644 --- a/src/test/java/land/oras/RegistryTest.java +++ b/src/test/java/land/oras/RegistryTest.java @@ -83,10 +83,7 @@ void before() { void shouldThrowIfUnableToFindOnAnyUnQualifiedSearchRegistry(@TempDir Path homeDir) throws Exception { // language=toml - String config = """ - unqualified-search-registries = ["%s"] - """ - .formatted(registry.getRegistry()); + String config = String.format("unqualified-search-registries = [\"%s\"]\n", registry.getRegistry()); TestUtils.createRegistriesConfFile(homeDir, config); @@ -103,10 +100,8 @@ void shouldThrowIfUnableToFindOnAnyUnQualifiedSearchRegistry(@TempDir Path homeD void shouldEnforceMultipleRegistriesWithDefaultEnforcingMode(@TempDir Path homeDir) throws Exception { // language=toml - String config = """ - unqualified-search-registries = ["%s", "localhost:5000"] - """ - .formatted(registry.getRegistry()); + String config = + String.format("unqualified-search-registries = [\"%s\", \"localhost:5000\"]\n", registry.getRegistry()); TestUtils.createRegistriesConfFile(homeDir, config); @@ -119,7 +114,7 @@ void shouldEnforceMultipleRegistriesWithDefaultEnforcingMode(@TempDir Path homeD e.getMessage() .startsWith( "Short name mode is set to ENFORCING/PERMISSION but multiple unqualified registries are configured"), - "Wrong exception message: got '%s'".formatted(e.getMessage())); + String.format("Wrong exception message: got '%s'", e.getMessage())); }); } @@ -128,12 +123,9 @@ void shouldEnforceMultipleRegistriesWithDefaultEnforcingMode(@TempDir Path homeD void shouldAllowMultipleRegistriesWithDisabledEnforcingMode(@TempDir Path homeDir) throws Exception { // language=toml - String config = - """ - short-name-mode = "disabled" - unqualified-search-registries = ["%s", "localhost:5000"] - """ - .formatted(registry.getRegistry()); + String config = String.format( + "short-name-mode = \"disabled\"\n" + "unqualified-search-registries = [\"%s\", \"localhost:5000\"]\n", + registry.getRegistry()); TestUtils.createRegistriesConfFile(homeDir, config); @@ -151,12 +143,9 @@ void shouldAllowMultipleRegistriesWithDisabledEnforcingMode(@TempDir Path homeDi void shouldEnforceMultipleRegistriesWithPermissiveEnforcingMode(@TempDir Path homeDir) throws Exception { // language=toml - String config = - """ - short-name-mode = "permissive" - unqualified-search-registries = ["%s", "localhost:5000"] - """ - .formatted(registry.getRegistry()); + String config = String.format( + "short-name-mode = \"permissive\"\n" + "unqualified-search-registries = [\"%s\", \"localhost:5000\"]\n", + registry.getRegistry()); TestUtils.createRegistriesConfFile(homeDir, config); @@ -169,7 +158,7 @@ void shouldEnforceMultipleRegistriesWithPermissiveEnforcingMode(@TempDir Path ho e.getMessage() .startsWith( "Short name mode is set to ENFORCING/PERMISSION but multiple unqualified registries are configured"), - "Wrong exception message: got '%s'".formatted(e.getMessage())); + String.format("Wrong exception message: got '%s'", e.getMessage())); }); } @@ -191,13 +180,8 @@ void shouldListRepositories() { void shouldListRepositoriesInsecure(@TempDir Path homeDir) throws Exception { // language=toml - String config = - """ - [[registry]] - location = "%s" - insecure = true - """ - .formatted(this.unsecureRegistry.getRegistry()); + String config = String.format( + "[[registry]]\n" + "location = \"%s\"\n" + "insecure = true\n", this.unsecureRegistry.getRegistry()); TestUtils.createRegistriesConfFile(homeDir, config); TestUtils.withHome(homeDir, () -> { @@ -290,13 +274,8 @@ void shouldCheckIfCanMount() throws IOException { void shouldMountWithInsecureRegistry(@TempDir Path homeDir) throws Exception { // language=toml - String config = - """ - [[registry]] - location = "%s" - insecure = true - """ - .formatted(this.unsecureRegistry.getRegistry()); + String config = String.format( + "[[registry]]\n" + "location = \"%s\"\n" + "insecure = true\n", this.unsecureRegistry.getRegistry()); TestUtils.createRegistriesConfFile(homeDir, config); TestUtils.withHome(homeDir, () -> { @@ -399,9 +378,9 @@ void shouldPushAndGetBlobThenDeleteWithSha512() { .defaults("myuser", "mypass") .withInsecure(true) .build(); - ContainerRef containerRef = ContainerRef.parse( - "%s/library/artifact-text@sha512:9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043" - .formatted(this.registry.getRegistry())); + ContainerRef containerRef = ContainerRef.parse(String.format( + "%s/library/artifact-text@sha512:9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043", + this.registry.getRegistry())); Layer layer = registry.pushBlob(containerRef, "hello".getBytes()); assertEquals( "sha512:9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043", @@ -427,7 +406,7 @@ void shouldPushAndGetBlobThenDeleteWithSha512() { void shouldFailWithoutAuthentication() { Registry registry = Registry.Builder.builder().insecure().build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-text".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-text", this.registry.getRegistry())); assertThrows(OrasException.class, () -> { registry.pushBlob(containerRef, "hello".getBytes()); }); @@ -437,7 +416,7 @@ void shouldFailWithoutAuthentication() { void shouldPushUnsecure() { Registry registry = Registry.Builder.builder().insecure().build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-text".formatted(this.unsecureRegistry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-text", this.unsecureRegistry.getRegistry())); registry.pushBlob(containerRef, "hello".getBytes()); } @@ -448,13 +427,8 @@ void shouldPushPullManifestsAndBlobsByUsingConfig(@TempDir Path homeDir) throws ExecutorService customExecutor = Executors.newSingleThreadExecutor(); // language=toml - String config = - """ - [[registry]] - location = "%s" - insecure = true - """ - .formatted(this.unsecureRegistry.getRegistry()); + String config = String.format( + "[[registry]]\n" + "location = \"%s\"\n" + "insecure = true\n", this.unsecureRegistry.getRegistry()); TestUtils.createRegistriesConfFile(homeDir, config); TestUtils.withHome(homeDir, () -> { @@ -462,7 +436,7 @@ void shouldPushPullManifestsAndBlobsByUsingConfig(@TempDir Path homeDir) throws .withExecutorService(customExecutor) .build(); ContainerRef containerRef = ContainerRef.parse( - "%s/library/artifact-text-manifest-blobs".formatted(this.unsecureRegistry.getRegistry())); + String.format("%s/library/artifact-text-manifest-blobs", this.unsecureRegistry.getRegistry())); registry.pushBlob(containerRef, "hello".getBytes()); registry.pushBlob(containerRef, "other-hello".getBytes()); @@ -523,7 +497,7 @@ void shouldFailWithoutExistingAuthentication() { Registry registry = Registry.Builder.builder().defaults().withInsecure(true).build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-text".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-text", this.registry.getRegistry())); assertThrows( OrasException.class, () -> { @@ -539,7 +513,7 @@ void shouldUploadAndFetchBlobThenDelete() throws IOException { .withInsecure(true) .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-text".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-text", this.registry.getRegistry())); Files.createFile(blobDir.resolve("temp.txt")); Files.writeString(blobDir.resolve("temp.txt"), "hello"); Layer layer = registry.pushBlob(containerRef, blobDir.resolve("temp.txt")); @@ -580,7 +554,7 @@ void shouldPushAndGetManifestThenDelete() { // Empty manifest ContainerRef containerRef = - ContainerRef.parse("%s/library/empty-manifest".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/empty-manifest", this.registry.getRegistry())); Layer emptyLayer = registry.pushBlob(containerRef, Layer.empty().getDataBytes()); Manifest emptyManifest = Manifest.empty().withLayers(List.of(Layer.fromDigest(emptyLayer.getDigest(), 2))); Manifest pushedManifest = registry.pushManifest(containerRef, emptyManifest); @@ -625,7 +599,8 @@ void shouldPushManifest() { .build(); // Empty manifest - ContainerRef containerRef = ContainerRef.parse("%s/library/empty-index".formatted(this.registry.getRegistry())); + ContainerRef containerRef = + ContainerRef.parse(String.format("%s/library/empty-index", this.registry.getRegistry())); Index emptyIndex = Index.fromManifests(List.of()); Index pushIndex = registry.pushIndex(containerRef, emptyIndex); @@ -650,13 +625,8 @@ void shouldPushManifest() { void shouldPushManifestWithRegistryConfig(@TempDir Path homeDir) throws Exception { // language=toml - String config = - """ - [[registry]] - location = "%s" - insecure = true - """ - .formatted(this.unsecureRegistry.getRegistry()); + String config = String.format( + "[[registry]]\n" + "location = \"%s\"\n" + "insecure = true\n", this.unsecureRegistry.getRegistry()); TestUtils.createRegistriesConfFile(homeDir, config); TestUtils.withHome(homeDir, () -> { @@ -664,7 +634,7 @@ void shouldPushManifestWithRegistryConfig(@TempDir Path homeDir) throws Exceptio // Empty manifest ContainerRef containerRef = - ContainerRef.parse("%s/library/empty-index".formatted(this.unsecureRegistry.getRegistry())); + ContainerRef.parse(String.format("%s/library/empty-index", this.unsecureRegistry.getRegistry())); Index emptyIndex = Index.fromManifests(List.of()); Index pushIndex = registry.pushIndex(containerRef, emptyIndex); @@ -690,16 +660,14 @@ void shouldPushManifestWithRegistryConfig(@TempDir Path homeDir) throws Exceptio void shouldPushManifestWithAlias(@TempDir Path homeDir) throws Exception { // language=toml - String config = - """ - [[registry]] - location = "%s" - insecure = true - - [aliases] - "my-library/my-namespace"="%s/test/bar" - """ - .formatted(this.unsecureRegistry.getRegistry(), this.unsecureRegistry.getRegistry()); + String config = String.format( + "[[registry]]\n" + + "location = \"%s\"\n" + + "insecure = true\n" + + "\n" + + "[aliases]\n" + + "\"my-library/my-namespace\"=\"%s/test/bar\"\n", + this.unsecureRegistry.getRegistry(), this.unsecureRegistry.getRegistry()); TestUtils.createRegistriesConfFile(homeDir, config); TestUtils.withHome(homeDir, () -> { @@ -732,11 +700,9 @@ void shouldPushManifestWithAlias(@TempDir Path homeDir) throws Exception { void shouldDetermineRegistryFromAlias(@TempDir Path homeDir) throws Exception { // language=toml - String config = """ - [aliases] - "my-library/my-namespace"="localhost/test" - """ - .formatted(this.unsecureRegistry.getRegistry()); + String config = String.format( + "[aliases]\n" + "\"my-library/my-namespace\"=\"localhost/test\"\n", + this.unsecureRegistry.getRegistry()); TestUtils.createRegistriesConfFile(homeDir, config); TestUtils.withHome(homeDir, () -> { @@ -781,7 +747,7 @@ void pullArtifactWithoutLayer() { .defaults("myuser", "mypass") .withInsecure(true) .build(); - ContainerRef containerRef1 = ContainerRef.parse("%s/empty-layers".formatted(this.registry.getRegistry())); + ContainerRef containerRef1 = ContainerRef.parse(String.format("%s/empty-layers", this.registry.getRegistry())); Config emptyConfig = Config.empty(); Manifest manifest1 = Manifest.empty().withConfig(emptyConfig); registry.pushConfig(containerRef1, emptyConfig); @@ -797,7 +763,8 @@ void pullArtifactShouldPullLayerWithTitleOnly() { .defaults("myuser", "mypass") .withInsecure(true) .build(); - ContainerRef containerRef1 = ContainerRef.parse("%s/empty-layers-title".formatted(this.registry.getRegistry())); + ContainerRef containerRef1 = + ContainerRef.parse(String.format("%s/empty-layers-title", this.registry.getRegistry())); Config emptyConfig = Config.empty(); Manifest manifest1 = Manifest.empty().withConfig(emptyConfig); Layer layer = registry.pushBlob(containerRef1, "hello".getBytes(StandardCharsets.UTF_8)) @@ -817,7 +784,8 @@ void pullArtifactShouldPullLayerWithNoTitle() { .defaults("myuser", "mypass") .withInsecure(true) .build(); - ContainerRef containerRef1 = ContainerRef.parse("%s/no-layers-title".formatted(this.registry.getRegistry())); + ContainerRef containerRef1 = + ContainerRef.parse(String.format("%s/no-layers-title", this.registry.getRegistry())); Config emptyConfig = Config.empty(); Manifest manifest1 = Manifest.empty().withConfig(emptyConfig); Layer layer = registry.pushBlob(containerRef1, "hello".getBytes(StandardCharsets.UTF_8)) @@ -839,8 +807,10 @@ void shouldPushComplexArtifactWithConfigMediaType() throws IOException { .build(); // Manifest 1 - ContainerRef containerRef1 = ContainerRef.parse("%s/library/manifest1".formatted(this.registry.getRegistry())); - ContainerRef containerRef2 = ContainerRef.parse("%s/library/manifest2".formatted(this.registry.getRegistry())); + ContainerRef containerRef1 = + ContainerRef.parse(String.format("%s/library/manifest1", this.registry.getRegistry())); + ContainerRef containerRef2 = + ContainerRef.parse(String.format("%s/library/manifest2", this.registry.getRegistry())); String content1 = "hello"; String content2 = "world"; @@ -895,8 +865,10 @@ void shouldPushComplexArtifactWithConfigArtifactType() throws IOException { .build(); // Manifest 1 - ContainerRef containerRef1 = ContainerRef.parse("%s/library/manifest1".formatted(this.registry.getRegistry())); - ContainerRef containerRef2 = ContainerRef.parse("%s/library/manifest2".formatted(this.registry.getRegistry())); + ContainerRef containerRef1 = + ContainerRef.parse(String.format("%s/library/manifest1", this.registry.getRegistry())); + ContainerRef containerRef2 = + ContainerRef.parse(String.format("%s/library/manifest2", this.registry.getRegistry())); String content1 = "hello"; String content2 = "world"; @@ -948,7 +920,7 @@ void shouldListReferrers(@TempDir Path homeDir) throws Exception { // Manifest 1 ContainerRef containerRef1 = - ContainerRef.parse("%s/library/manifest1:latest".formatted(this.unsecureRegistry.getRegistry())); + ContainerRef.parse(String.format("%s/library/manifest1:latest", this.unsecureRegistry.getRegistry())); String content1 = "hello"; String content2 = "world"; @@ -976,10 +948,10 @@ void shouldListReferrers(@TempDir Path homeDir) throws Exception { .withArtifactType(ArtifactType.from("text/plain")); // Push second manifest with its digest - ContainerRef containerRef2 = ContainerRef.parse("%s/library/manifest1@%s" - .formatted( - this.unsecureRegistry.getRegistry(), - SupportedAlgorithm.SHA256.digest(manifest2.toJson().getBytes(StandardCharsets.UTF_8)))); + ContainerRef containerRef2 = ContainerRef.parse(String.format( + "%s/library/manifest1@%s", + this.unsecureRegistry.getRegistry(), + SupportedAlgorithm.SHA256.digest(manifest2.toJson().getBytes(StandardCharsets.UTF_8)))); registry.pushManifest(containerRef2, manifest2); // Pull via artifact 2 @@ -1025,13 +997,8 @@ void shouldListReferrers(@TempDir Path homeDir) throws Exception { // Use config // language=toml - String config = - """ - [[registry]] - location = "%s" - insecure = true - """ - .formatted(this.unsecureRegistry.getRegistry()); + String config = String.format( + "[[registry]]\n" + "location = \"%s\"\n" + "insecure = true\n", this.unsecureRegistry.getRegistry()); TestUtils.createRegistriesConfFile(homeDir, config); TestUtils.withHome(homeDir, () -> { @@ -1054,7 +1021,8 @@ void testShouldFailReferrerWithoutDigest() { OrasException.class, () -> { registry.getReferrers( - ContainerRef.parse("%s/library/manifest1".formatted(this.registry.getRegistry())), null); + ContainerRef.parse(String.format("%s/library/manifest1", this.registry.getRegistry())), + null); }, "Digest is required to get referrers"); } @@ -1068,7 +1036,7 @@ void testShouldCopyIndexOfIndexAndUseMounting() throws IOException { .build(); ContainerRef containerSource = - ContainerRef.parse("%s/library/artifact-mount-source".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-mount-source", this.registry.getRegistry())); // Create files Path file1 = blobDir.resolve("source1.txt"); @@ -1094,7 +1062,7 @@ void testShouldCopyIndexOfIndexAndUseMounting() throws IOException { // Copy to other registry ContainerRef containerTarget = - ContainerRef.parse("%s/library/artifact-mount-target".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-mount-target", this.registry.getRegistry())); CopyUtils.copy( registry, containerSource.withTag("index2"), @@ -1114,7 +1082,7 @@ void testShouldCopyIndexOfIndexArtifactFromRegistryIntoRegistryWithDeepCopy() th .build(); ContainerRef containerSource = - ContainerRef.parse("%s/library/artifact-index-source".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-index-source", this.registry.getRegistry())); // Create files Path file1 = blobDir.resolve("source1.txt"); @@ -1142,7 +1110,7 @@ void testShouldCopyIndexOfIndexArtifactFromRegistryIntoRegistryWithDeepCopy() th try (RegistryContainer otherRegistryContainer = new RegistryContainer()) { otherRegistryContainer.start(); ContainerRef containerTarget = ContainerRef.parse( - "%s/library/artifact-index-target".formatted(otherRegistryContainer.getRegistry())); + String.format("%s/library/artifact-index-target", otherRegistryContainer.getRegistry())); CopyUtils.copy( registry, containerSource.withTag("index2"), @@ -1163,7 +1131,7 @@ void testShouldCopySingleArtifactFromRegistryIntoRegistry() throws IOException { .build(); ContainerRef containerSource = - ContainerRef.parse("%s/library/artifact-source:foo".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-source:foo", this.registry.getRegistry())); Path file1 = blobDir.resolve("source.txt"); Files.writeString(file1, "foobar"); @@ -1175,7 +1143,7 @@ void testShouldCopySingleArtifactFromRegistryIntoRegistry() throws IOException { try (RegistryContainer otherRegistryContainer = new RegistryContainer()) { otherRegistryContainer.start(); ContainerRef containerTarget = ContainerRef.parse( - "%s/library/artifact-target:bar".formatted(otherRegistryContainer.getRegistry())); + String.format("%s/library/artifact-target:bar", otherRegistryContainer.getRegistry())); CopyUtils.copy(registry, containerSource, registry, containerTarget, CopyUtils.CopyOptions.shallow()); registry.pullArtifact(containerTarget, artifactDir, true); assertEquals("foobar", Files.readString(artifactDir.resolve("source.txt"))); @@ -1193,7 +1161,7 @@ void testShouldCopySingleFromDigest() throws IOException { .build(); ContainerRef containerSource = - ContainerRef.parse("%s/library/artifact-source:foo".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-source:foo", this.registry.getRegistry())); Path file1 = blobDir.resolve("source.txt"); Files.writeString(file1, "foobar"); @@ -1204,8 +1172,8 @@ void testShouldCopySingleFromDigest() throws IOException { // Copy to other registry try (RegistryContainer otherRegistryContainer = new RegistryContainer()) { otherRegistryContainer.start(); - ContainerRef containerTarget = - ContainerRef.parse("%s/library/artifact-target".formatted(otherRegistryContainer.getRegistry())); + ContainerRef containerTarget = ContainerRef.parse( + String.format("%s/library/artifact-target", otherRegistryContainer.getRegistry())); CopyUtils.copy( registry, containerSource.withDigest(manifest.getDigest()), @@ -1224,16 +1192,14 @@ void testShouldCopySingleFromDigest() throws IOException { void testShouldArtifactWithAlias(@TempDir Path homeDir) throws Exception { // language=toml - String config = - """ - [aliases] - "the-target" = "%s/test/artifact-target" - - [[registry]] - location = "%s" - insecure = true - """ - .formatted(this.unsecureRegistry.getRegistry(), this.unsecureRegistry.getRegistry()); + String config = String.format( + "[aliases]\n" + + "\"the-target\" = \"%s/test/artifact-target\"\n" + + "\n" + + "[[registry]]\n" + + "location = \"%s\"\n" + + "insecure = true\n", + this.unsecureRegistry.getRegistry(), this.unsecureRegistry.getRegistry()); TestUtils.createRegistriesConfFile(homeDir, config); // Copy to same registry @@ -1268,25 +1234,22 @@ void testShouldCopyFromAliasToAlias(@TempDir Path homeDir) throws Exception { otherRegistryContainer.start(); // language=toml - String config = - """ - [aliases] - "the-source" = "%s/test/artifact-source" - "the-target" = "%s/test/artifact-target" - - [[registry]] - location = "%s" - insecure = true - - [[registry]] - location = "%s" - insecure = true - """ - .formatted( - this.unsecureRegistry.getRegistry(), - otherRegistryContainer.getRegistry(), - this.unsecureRegistry.getRegistry(), - otherRegistryContainer.getRegistry()); + String config = String.format( + "[aliases]\n" + + "\"the-source\" = \"%s/test/artifact-source\"\n" + + "\"the-target\" = \"%s/test/artifact-target\"\n" + + "\n" + + "[[registry]]\n" + + "location = \"%s\"\n" + + "insecure = true\n" + + "\n" + + "[[registry]]\n" + + "location = \"%s\"\n" + + "insecure = true\n", + this.unsecureRegistry.getRegistry(), + otherRegistryContainer.getRegistry(), + this.unsecureRegistry.getRegistry(), + otherRegistryContainer.getRegistry()); TestUtils.createRegistriesConfFile(homeDir, config); // Copy to same registry @@ -1324,7 +1287,7 @@ void testShouldCopyFromOciLayoutToRegistryNonRecursive() throws IOException { .withInsecure(true) .build(); ContainerRef targetRef = - ContainerRef.parse("%s/library/copied-from-oci-layout".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/copied-from-oci-layout", this.registry.getRegistry())); LayoutRef layoutRef = LayoutRef.parse("src/test/resources/oci/subject:latest"); OCILayout ociLayout = @@ -1358,7 +1321,7 @@ void testShouldCopyFromOciLayoutToRegistryRecursive() throws IOException { // Registry to copy Registry registry = Registry.builder().withInsecure(true).build(); ContainerRef targetRef = ContainerRef.parse( - "%s/library/copied-from-oci-layout-recursive".formatted(this.unsecureRegistry.getRegistry())); + String.format("%s/library/copied-from-oci-layout-recursive", this.unsecureRegistry.getRegistry())); LayoutRef layoutRef = LayoutRef.parse("src/test/resources/oci/subject:latest"); OCILayout ociLayout = @@ -1390,7 +1353,7 @@ void testNotFailToPullArtifactFromImage() { .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-image-pull".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-image-pull", this.registry.getRegistry())); Layer emptyLayer = registry.pushBlob(containerRef, Layer.empty().getDataBytes()); @@ -1422,9 +1385,9 @@ void testShouldPushAndPullMinimalArtifact() throws IOException { .withInsecure(true) .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-full".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-full", this.registry.getRegistry())); ContainerRef unknown = - ContainerRef.parse("%s/library/artifact-full:unknown".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-full:unknown", this.registry.getRegistry())); Path file1 = blobDir.resolve("file1.txt"); Files.writeString(file1, "foobar"); @@ -1495,7 +1458,7 @@ void testShouldPushMinimalArtifactThenAttachArtifactToManifest() throws IOExcept .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-maven".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-maven", this.registry.getRegistry())); Path pomFile = blobDir.resolve("pom.xml"); Files.writeString(pomFile, "my pom file"); @@ -1536,7 +1499,7 @@ void testShouldPushMinimalArtifactThenAttachArtifactToIndex() throws IOException .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-maven".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-maven", this.registry.getRegistry())); Path pomFile = blobDir.resolve("pom.xml"); Files.writeString(pomFile, "my pom file"); @@ -1585,7 +1548,7 @@ void testShouldArtifactWithAnnotations() throws IOException { .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-maven".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-maven", this.registry.getRegistry())); Path pomFile = blobDir.resolve("pom.xml"); Files.writeString(pomFile, "my pom file"); @@ -1628,7 +1591,7 @@ void testShouldPushAndPullCompressedZipDirectoryButNotUnpackIt() throws IOExcept .withInsecure(true) .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-zip".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-zip", this.registry.getRegistry())); Path file1 = blobDir.resolve("file1.txt"); Path file2 = blobDir.resolve("file2.txt"); @@ -1653,7 +1616,7 @@ void testShouldPushAndPullCompressedZipDirectoryButNotUnpackIt() throws IOExcept // Title must container filename assertEquals( - "%s.%s".formatted(blobDir.getFileName().toString(), SupportedCompression.ZIP.getFileExtension()), + String.format("%s.%s", blobDir.getFileName().toString(), SupportedCompression.ZIP.getFileExtension()), annotations.get(Const.ANNOTATION_TITLE)); // Pull @@ -1674,7 +1637,7 @@ void testShouldPushAndPullCompressedTarGzDirectory() throws IOException { .withInsecure(true) .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-full".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-full", this.registry.getRegistry())); Path file1 = blobDir.resolve("file1.txt"); Path file2 = blobDir.resolve("file2.txt"); @@ -1719,7 +1682,7 @@ void testShouldPushAndPullUncompressedTarDirectoryWithAbsolutePath() throws IOEx .withInsecure(true) .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-not-compressed".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-not-compressed", this.registry.getRegistry())); Path file1 = blobDir.resolve("file1.txt"); Path file2 = blobDir.resolve("file2.txt"); @@ -1764,7 +1727,7 @@ void testShouldPushAndPullUncompressedTarDirectoryWithRelativePath() throws IOEx .withInsecure(true) .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-relative-path".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-relative-path", this.registry.getRegistry())); // Source Manifest manifest = registry.pushArtifact( @@ -1801,7 +1764,7 @@ void testShouldPushAndPullCompressedZstdDirectory() throws IOException { .withInsecure(true) .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-full".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-full", this.registry.getRegistry())); Path file1 = blobDir.resolve("file1.txt"); Path file2 = blobDir.resolve("file2.txt"); @@ -1846,7 +1809,7 @@ void shouldFailToPushDirectoryWithInvalidCompression() throws IOException { .withInsecure(true) .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-full".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-full", this.registry.getRegistry())); assertThrows( OrasException.class, @@ -1863,7 +1826,7 @@ void shouldPushAndGetBlobStreamSha256() throws IOException { .withInsecure(true) .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-stream".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-stream", this.registry.getRegistry())); // Create a file with test data to get accurate stream size Path testFile = Files.createTempFile("test-data-", ".tmp"); @@ -1899,9 +1862,9 @@ void shouldPushAndGetBlobStreamWithSha512() throws IOException { .defaults("myuser", "mypass") .withInsecure(true) .build(); - ContainerRef containerRef = ContainerRef.parse( - "%s/library/artifact-stream-sha512@sha512:ea0d8750d01f5fbd0da5d020d981b377fa2177874751063cb3da2117e481720774c0d985845a56c32ee6dde144901d92b2bdc8d0cb02373da141241aa2409859" - .formatted(this.registry.getRegistry())); + ContainerRef containerRef = ContainerRef.parse(String.format( + "%s/library/artifact-stream-sha512@sha512:ea0d8750d01f5fbd0da5d020d981b377fa2177874751063cb3da2117e481720774c0d985845a56c32ee6dde144901d92b2bdc8d0cb02373da141241aa2409859", + this.registry.getRegistry())); // Create a file with test data to get accurate stream size Path testFile = Files.createTempFile("test-data-", ".tmp"); @@ -1938,7 +1901,7 @@ void shouldHandleExistingBlobInStreamPush() throws IOException { .withInsecure(true) .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-stream".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-stream", this.registry.getRegistry())); // Create test file Path testFile = Files.createTempFile("test-data-", ".tmp"); @@ -1971,7 +1934,7 @@ void shouldHandleIOExceptionInStreamPush() throws IOException { .withInsecure(true) .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-stream".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-stream", this.registry.getRegistry())); // Create a failing input stream InputStream failingStream = new InputStream() { @@ -1994,7 +1957,7 @@ void shouldHandleNonExistentBlobInGetStreamSha256() { .withInsecure(true) .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-stream".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-stream", this.registry.getRegistry())); // Try to get non-existent blob String nonExistentDigest = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; @@ -2010,7 +1973,7 @@ void shouldHandleNonExistentBlobInGetStreamSha512() { .withInsecure(true) .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-stream".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-stream", this.registry.getRegistry())); // Try to get non-existent blob String nonExistentDigest = @@ -2027,7 +1990,7 @@ void shouldHandleLargeStreamContent() throws IOException { .withInsecure(true) .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-stream".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-stream", this.registry.getRegistry())); // Create temp file with 5MB of random data Path largeFile = Files.createTempFile("large-test-", ".tmp"); @@ -2055,7 +2018,7 @@ void shouldPushBlobChunkedFromPath() throws IOException { .withInsecure(true) .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-chunked-path".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-chunked-path", this.registry.getRegistry())); // Create a file with known content byte[] content = "hello chunked world".getBytes(StandardCharsets.UTF_8); @@ -2085,8 +2048,8 @@ void shouldPushBlobChunkedFromPathWithSingleChunk() throws IOException { .defaults("myuser", "mypass") .withInsecure(true) .build(); - ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-chunked-path-single".formatted(this.registry.getRegistry())); + ContainerRef containerRef = ContainerRef.parse( + String.format("%s/library/artifact-chunked-path-single", this.registry.getRegistry())); // Content that fits in one chunk byte[] content = "hi".getBytes(StandardCharsets.UTF_8); @@ -2108,7 +2071,7 @@ void shouldPushBlobChunkedFromInputStream() { .withInsecure(true) .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-chunked-stream".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-chunked-stream", this.registry.getRegistry())); byte[] content = "hello chunked stream".getBytes(StandardCharsets.UTF_8); String expectedDigest = SupportedAlgorithm.SHA256.digest(content); @@ -2138,7 +2101,7 @@ void shouldPushLargeBlobChunkedFromPath() throws IOException { .withInsecure(true) .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-chunked-large".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-chunked-large", this.registry.getRegistry())); // 1 MB of random content byte[] content = new byte[1024 * 1024]; @@ -2168,7 +2131,7 @@ void shouldFailChunkedUploadWithMissingDigestOnInputStream() { .withInsecure(true) .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-chunked-err".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-chunked-err", this.registry.getRegistry())); OrasException e = assertThrows( OrasException.class, @@ -2184,7 +2147,7 @@ void shouldFailChunkedUploadWithInvalidChunkSize() throws IOException { .withInsecure(true) .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-chunked-err".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/artifact-chunked-err", this.registry.getRegistry())); byte[] content = "data".getBytes(StandardCharsets.UTF_8); Path blobFile = blobDir.resolve("err.txt"); @@ -2207,13 +2170,8 @@ void shouldFailChunkedUploadWithInvalidChunkSize() throws IOException { void shouldPushBlobChunkedFromPathViaInsecureRegistryConfig(@TempDir Path homeDir) throws Exception { // Insecure config - String config = - """ - [[registry]] - location = "%s" - insecure = true - """ - .formatted(this.unsecureRegistry.getRegistry()); + String config = String.format( + "[[registry]]\n" + "location = \"%s\"\n" + "insecure = true\n", this.unsecureRegistry.getRegistry()); TestUtils.createRegistriesConfFile(homeDir, config); TestUtils.withHome(homeDir, () -> { @@ -2246,13 +2204,8 @@ void shouldPushBlobChunkedFromPathViaInsecureRegistryConfig(@TempDir Path homeDi void shouldPushBlobChunkedFromStreamViaInsecureRegistryConfig(@TempDir Path homeDir) throws Exception { // Insecure config - String config = - """ - [[registry]] - location = "%s" - insecure = true - """ - .formatted(this.unsecureRegistry.getRegistry()); + String config = String.format( + "[[registry]]\n" + "location = \"%s\"\n" + "insecure = true\n", this.unsecureRegistry.getRegistry()); TestUtils.createRegistriesConfFile(homeDir, config); TestUtils.withHome(homeDir, () -> { @@ -2282,7 +2235,7 @@ void testShouldCopyIndexWithPlatformFilter() throws IOException { .build(); ContainerRef containerSource = - ContainerRef.parse("%s/library/multi-platform-source".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/multi-platform-source", this.registry.getRegistry())); // Push two manifests with different content Path fileAmd64 = blobDir.resolve("amd64.txt"); @@ -2304,8 +2257,8 @@ void testShouldCopyIndexWithPlatformFilter() throws IOException { registry.pushIndex(containerSource.withTag("latest"), sourceIndex); // Copy only linux/amd64 to target - ContainerRef containerTarget = - ContainerRef.parse("%s/library/multi-platform-target:latest".formatted(this.registry.getRegistry())); + ContainerRef containerTarget = ContainerRef.parse( + String.format("%s/library/multi-platform-target:latest", this.registry.getRegistry())); CopyUtils.copy( registry, containerSource.withTag("latest"), @@ -2340,7 +2293,7 @@ void testShouldThrowWhenPlatformFilterMatchesNothing() throws IOException { .build(); ContainerRef containerSource = - ContainerRef.parse("%s/library/no-match-source".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/no-match-source", this.registry.getRegistry())); // Push a single manifest and build an index with linux/amd64 Path file = blobDir.resolve("no-match.txt"); @@ -2352,7 +2305,7 @@ void testShouldThrowWhenPlatformFilterMatchesNothing() throws IOException { registry.pushIndex(containerSource.withTag("latest"), sourceIndex); ContainerRef containerTarget = - ContainerRef.parse("%s/library/no-match-target:latest".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/no-match-target:latest", this.registry.getRegistry())); // Filtering for a platform not present in the index must throw assertThrows( @@ -2372,8 +2325,8 @@ void shouldPushArtifactWithChunkedOptions() throws IOException { .defaults("myuser", "mypass") .withInsecure(true) .build(); - ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-push-options-chunked".formatted(this.registry.getRegistry())); + ContainerRef containerRef = ContainerRef.parse( + String.format("%s/library/artifact-push-options-chunked", this.registry.getRegistry())); Path file = blobDir.resolve("chunked-artifact.txt"); Files.writeString(file, "hello chunked artifact"); @@ -2398,7 +2351,7 @@ void shouldPushArtifactWithChunkedOptionsCustomChunkSizeAndArtifactType() throws .withInsecure(true) .build(); ContainerRef containerRef = ContainerRef.parse( - "%s/library/artifact-push-options-chunked-type".formatted(this.registry.getRegistry())); + String.format("%s/library/artifact-push-options-chunked-type", this.registry.getRegistry())); Path file = blobDir.resolve("chunked-artifact-type.txt"); Files.writeString(file, "chunked with artifact type"); @@ -2421,7 +2374,7 @@ void shouldPushArtifactWithChunkedOptionsAndAnnotations() throws IOException { .withInsecure(true) .build(); ContainerRef containerRef = ContainerRef.parse( - "%s/library/artifact-push-options-chunked-annotations".formatted(this.registry.getRegistry())); + String.format("%s/library/artifact-push-options-chunked-annotations", this.registry.getRegistry())); Path file = blobDir.resolve("chunked-artifact-annotations.txt"); Files.writeString(file, "chunked with annotations"); @@ -2461,7 +2414,7 @@ void shouldBlockPullWithRejectPolicy(@TempDir Path policyDir) throws IOException .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/policy-reject-test".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/policy-reject-test", this.registry.getRegistry())); Path file = blobDir.resolve("policy-test.txt"); Files.writeString(file, "This artifact should be blocked"); @@ -2484,9 +2437,7 @@ void shouldBlockPullWithRejectPolicy(@TempDir Path policyDir) throws IOException void shouldBlockPullWithPolicyFromPath() throws IOException { // Create a temporary policy file with reject-all Path policyFile = Files.createTempFile("policy", ".json"); - Files.writeString(policyFile, """ - {"default": [{"type": "reject"}]} - """); + Files.writeString(policyFile, "{\"default\": [{\"type\": \"reject\"}]}\n"); // Create a registry with policy loaded from file path Registry registryWithPolicy = Registry.Builder.builder() @@ -2502,7 +2453,7 @@ void shouldBlockPullWithPolicyFromPath() throws IOException { .build(); ContainerRef containerRef = - ContainerRef.parse("%s/library/policy-path-test".formatted(this.registry.getRegistry())); + ContainerRef.parse(String.format("%s/library/policy-path-test", this.registry.getRegistry())); Path file = blobDir.resolve("policy-path-test.txt"); Files.writeString(file, "This artifact should be blocked by policy from file"); @@ -2514,7 +2465,7 @@ void shouldBlockPullWithPolicyFromPath() throws IOException { // Pull fails with policy from file - verify exact error message format OrasException ex = assertThrows(OrasException.class, () -> registryWithPolicy.getManifest(containerRef)); String expectedMessage = - "Access to container reference %s is blocked by registry configuration".formatted(containerRef); + String.format("Access to container reference %s is blocked by registry configuration", containerRef); assertEquals(expectedMessage, ex.getMessage()); Files.deleteIfExists(policyFile); @@ -2525,11 +2476,7 @@ private Path createRejectPolicy(Path tempDir) throws IOException { Files.writeString( output, // language=json - """ - { - "default": [{"type": "reject"}] - } - """); + "{\n" + " \"default\": [{\"type\": \"reject\"}]\n" + "}\n"); return output; } @@ -2538,11 +2485,7 @@ private Path createAcceptAllPolicy(Path tempDir) throws IOException { Files.writeString( output, // language=json - """ - { - "default": [{"type": "insecureAcceptAnything"}] - } - """); + "{\n" + " \"default\": [{\"type\": \"insecureAcceptAnything\"}]\n" + "}\n"); return output; } } diff --git a/src/test/java/land/oras/RegistryTlsTest.java b/src/test/java/land/oras/RegistryTlsTest.java index e67c6b43..1be78745 100644 --- a/src/test/java/land/oras/RegistryTlsTest.java +++ b/src/test/java/land/oras/RegistryTlsTest.java @@ -31,7 +31,6 @@ import java.security.KeyPair; import java.util.List; import java.util.Map; -import javax.net.ssl.SSLHandshakeException; import land.oras.exception.OrasException; import land.oras.utils.SupportedAlgorithm; import land.oras.utils.TlsUtils; @@ -91,7 +90,7 @@ void shouldFailWithoutCaCertificate() { Registry.builder().withRegistry(tlsRegistry.getRegistry()).build(); OrasException exception = assertThrows(OrasException.class, registry::getRepositories); - assertInstanceOf(SSLHandshakeException.class, exception.getCause()); + assertInstanceOf(IOException.class, exception.getCause()); } @Test @@ -102,7 +101,7 @@ void shouldFailWithWrongCaCertificate() { .build(); OrasException exception = assertThrows(OrasException.class, registry::getRepositories); - assertInstanceOf(SSLHandshakeException.class, exception.getCause()); + assertInstanceOf(IOException.class, exception.getCause()); } @Test @@ -128,7 +127,7 @@ void shouldBeSecureAfterAsSecure() { // asSecure() resets both insecure and skipTlsVerify — now proper TLS is enforced Registry secureRegistry = skipVerifyRegistry.asSecure(); OrasException e = assertThrows(OrasException.class, secureRegistry::getRepositories); - assertInstanceOf(SSLHandshakeException.class, e.getCause()); + assertInstanceOf(IOException.class, e.getCause()); // Verify the same host works when the CA cert is provided Registry secureWithCa = Registry.builder() @@ -144,13 +143,8 @@ void shouldDowngradeToSecureWhenConfigExplicitlyNotInsecure(@TempDir Path homeDi // Authoritative parent // language=toml - String registriesConf = - """ - [[registry]] - location = "%s" - insecure = false - """ - .formatted(tlsRegistry.getRegistry()); + String registriesConf = String.format( + "[[registry]]\n" + "location = \"%s\"\n" + "insecure = false\n", tlsRegistry.getRegistry()); TestUtils.createRegistriesConfFile(homeDir, registriesConf); @@ -173,11 +167,7 @@ void shouldUpgradeToSecureAllOperationsToSecureWhenConfigDefaultsToSecure( // Registry is set to secure by default // language=toml - String registriesConf = """ - [[registry]] - location = "%s" - """ - .formatted(tlsRegistry.getRegistry()); + String registriesConf = String.format("[[registry]]\n" + "location = \"%s\"\n", tlsRegistry.getRegistry()); TestUtils.createRegistriesConfFile(homeDir, registriesConf); Path testFile = blobDir.resolve("downgrade1.txt"); @@ -299,21 +289,19 @@ void shouldNotDowngradeSecureMirrorWhenRegistryLevelInsecureSetForMirrorHost( // connection to plaintext HTTP. The HTTPS-only container would refuse a plaintext request, so // a successful manifest fetch proves the connection stayed on HTTPS. // language=toml - String registriesConf = - """ - [[registry]] - prefix = "localhost:59998" - location = "localhost:59998" - - [[registry.mirror]] - location = "%s" - - [[registry]] - prefix = "%s" - location = "%s" - insecure = true - """ - .formatted(mirror, mirror, mirror); + String registriesConf = String.format( + "[[registry]]\n" + + "prefix = \"localhost:59998\"\n" + + "location = \"localhost:59998\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"%s\"\n" + + "\n" + + "[[registry]]\n" + + "prefix = \"%s\"\n" + + "location = \"%s\"\n" + + "insecure = true\n", + mirror, mirror, mirror); TestUtils.createRegistriesConfFile(homeDir, registriesConf); diff --git a/src/test/java/land/oras/RegistryWireMockTest.java b/src/test/java/land/oras/RegistryWireMockTest.java index 801b9b7e..1fdc5b39 100644 --- a/src/test/java/land/oras/RegistryWireMockTest.java +++ b/src/test/java/land/oras/RegistryWireMockTest.java @@ -104,15 +104,16 @@ void shouldPassBearerTokenWithExternalRequestedToken(WireMockRuntimeInfo wmRunti .withHeader("Authorization", equalTo("Bearer invalid-token")) .willReturn(WireMock.unauthorized())); - registry.getTags(ContainerRef.parse("%s/library/artifact-text" - .formatted(wmRuntimeInfo.getHttpBaseUrl().replace("http://", "")))); + registry.getTags(ContainerRef.parse(String.format( + "%s/library/artifact-text", wmRuntimeInfo.getHttpBaseUrl().replace("http://", "")))); // Ensure it fail with invalid token final Registry newRegistry = registry.withAuthToken("invalid-token"); OrasException e = assertThrows( OrasException.class, - () -> newRegistry.getTags(ContainerRef.parse("%s/library/artifact-text" - .formatted(wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""))))); + () -> newRegistry.getTags(ContainerRef.parse(String.format( + "%s/library/artifact-text", + wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""))))); assertEquals(401, e.getStatusCode()); } @@ -132,8 +133,8 @@ void shouldFailToGetManifestOn403(WireMockRuntimeInfo wmRuntimeInfo) { .withInsecure(true) .build(); - ContainerRef containerRef = - ContainerRef.parse("localhost:%d/library/some-artifact:latest".formatted(wmRuntimeInfo.getHttpPort())); + ContainerRef containerRef = ContainerRef.parse( + String.format("localhost:%d/library/some-artifact:latest", wmRuntimeInfo.getHttpPort())); OrasException exception = assertThrows(OrasException.class, () -> registry.getManifest(containerRef)); assertEquals(403, exception.getStatusCode()); } @@ -145,12 +146,13 @@ void shouldRedirectWhenDownloadingBlob(WireMockRuntimeInfo wmRuntimeInfo) { // Return data from wiremock WireMock wireMock = wmRuntimeInfo.getWireMock(); - wireMock.register(WireMock.any(WireMock.urlEqualTo("/v2/library/artifact-text/blobs/%s".formatted(digest))) - .willReturn(WireMock.temporaryRedirect("http://localhost:%d/v2/library/artifact-text/blobs/sha256:other" - .formatted(wmRuntimeInfo.getHttpPort())))); + wireMock.register(WireMock.any(WireMock.urlEqualTo(String.format("/v2/library/artifact-text/blobs/%s", digest))) + .willReturn(WireMock.temporaryRedirect(String.format( + "http://localhost:%d/v2/library/artifact-text/blobs/sha256:other", + wmRuntimeInfo.getHttpPort())))); // Return blob on new location - wireMock.register(head(WireMock.urlEqualTo("/v2/library/artifact-text/blobs/%s".formatted(digest))) + wireMock.register(head(WireMock.urlEqualTo(String.format("/v2/library/artifact-text/blobs/%s", digest))) .willReturn(WireMock.ok())); wireMock.register(WireMock.get(WireMock.urlEqualTo("/v2/library/artifact-text/blobs/sha256:other")) .willReturn( @@ -163,7 +165,7 @@ void shouldRedirectWhenDownloadingBlob(WireMockRuntimeInfo wmRuntimeInfo) { .build(); ContainerRef containerRef = - ContainerRef.parse("localhost:%d/library/artifact-text".formatted(wmRuntimeInfo.getHttpPort())); + ContainerRef.parse(String.format("localhost:%d/library/artifact-text", wmRuntimeInfo.getHttpPort())); byte[] blob = registry.getBlob(containerRef.withDigest(digest)); assertEquals("blob-data", new String(blob)); } @@ -187,8 +189,8 @@ void mountBlobShouldReturnFalseOn202(WireMockRuntimeInfo wmRuntimeInfo) throws I .withInsecure(true) .build(); - ContainerRef containerRef = ContainerRef.parse( - "%s/test@sha512:12345".formatted(wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""))); + ContainerRef containerRef = ContainerRef.parse(String.format( + "%s/test@sha512:12345", wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""))); assertFalse(registry.mountBlob(containerRef, containerRef), "Mount blob should return false"); } @@ -213,9 +215,9 @@ void shouldRedirectWhenPushingBlob(WireMockRuntimeInfo wmRuntimeInfo) throws IOE .withInsecure(true) .build(); - ContainerRef containerRef = ContainerRef.parse( - "localhost:%d/library/artifact-redirect@sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" - .formatted(wmRuntimeInfo.getHttpPort())); + ContainerRef containerRef = ContainerRef.parse(String.format( + "localhost:%d/library/artifact-redirect@sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", + wmRuntimeInfo.getHttpPort())); registry.pushBlob(containerRef, "hello".getBytes()); // Via file @@ -234,12 +236,13 @@ void shouldNotSendAuthHeaderOnRedirectToDifferentDomain(WireMockRuntimeInfo wmRu redirectTarget.start(); try { - String redirectUrl = "http://localhost:%d/v2/other/blobs/sha256:other".formatted(redirectTarget.port()); + String redirectUrl = + String.format("http://localhost:%d/v2/other/blobs/sha256:other", redirectTarget.port()); WireMock mainMock = wmRuntimeInfo.getWireMock(); // Main mock responds with redirect to different domain - mainMock.register(WireMock.any(WireMock.urlEqualTo("/v2/library/artifact/blobs/%s".formatted(digest))) + mainMock.register(WireMock.any(WireMock.urlEqualTo(String.format("/v2/library/artifact/blobs/%s", digest))) .willReturn(WireMock.temporaryRedirect(redirectUrl))); // Secondary server returns blob, we inspect headers here @@ -258,7 +261,7 @@ void shouldNotSendAuthHeaderOnRedirectToDifferentDomain(WireMockRuntimeInfo wmRu .build(); ContainerRef containerRef = - ContainerRef.parse("localhost:%d/library/artifact".formatted(wmRuntimeInfo.getHttpPort())); + ContainerRef.parse(String.format("localhost:%d/library/artifact", wmRuntimeInfo.getHttpPort())); byte[] blob = registry.getBlob(containerRef.withDigest(digest)); assertEquals("blob-data", new String(blob)); @@ -282,26 +285,20 @@ void shouldNotForwardParentCredentialsToInsecureMirror(WireMockRuntimeInfo wmRun // WireMock server. The parent registry is configured with static basic-auth credentials — those // credentials must NOT be forwarded to the mirror over plaintext HTTP. // language=toml - String config = - """ - [[registry]] - prefix = "localhost:59998" - location = "localhost:59998" - - [[registry.mirror]] - location = "%s" - insecure = true - """ - .formatted(mirrorHost); + String config = String.format( + "[[registry]]\n" + + "prefix = \"localhost:59998\"\n" + + "location = \"localhost:59998\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"%s\"\n" + + "insecure = true\n", + mirrorHost); TestUtils.createRegistriesConfFile(homeDir4, config); // language=json String manifestJson = - """ - {"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json",\ - "config":{"mediaType":"application/vnd.oci.empty.v1+json",\ - "digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2},\ - "layers":[]}"""; + "{\"schemaVersion\":2,\"mediaType\":\"application/vnd.oci.image.manifest.v1+json\",\"config\":{\"mediaType\":\"application/vnd.oci.empty.v1+json\",\"digest\":\"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a\",\"size\":2},\"layers\":[]}"; WireMock wireMock = wmRuntimeInfo.getWireMock(); String manifestPath = "/v2/library/cred-mirror/manifests/v1"; @@ -345,8 +342,9 @@ void shouldListTags(WireMockRuntimeInfo wmRuntimeInfo) { .build(); // Test - List tags = registry.getTags(ContainerRef.parse("%s/library/artifact-text" - .formatted(wmRuntimeInfo.getHttpBaseUrl().replace("http://", "")))) + List tags = registry.getTags(ContainerRef.parse(String.format( + "%s/library/artifact-text", + wmRuntimeInfo.getHttpBaseUrl().replace("http://", "")))) .tags(); // Assert @@ -371,8 +369,9 @@ void shouldListTagsWithLimit(WireMockRuntimeInfo wmRuntimeInfo) { // Test List tags = registry.getTags( - ContainerRef.parse("%s/library/artifact-text" - .formatted(wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""))), + ContainerRef.parse(String.format( + "%s/library/artifact-text", + wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""))), 1, null) .tags(); @@ -388,13 +387,7 @@ void shouldListTagsWithConfig(WireMockRuntimeInfo wmRuntimeInfo) throws Exceptio String registryAsString = wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""); // language=toml - String config = - """ - [[registry]] - location = "%s" - insecure = true - """ - .formatted(registryAsString); + String config = String.format("[[registry]]\n" + "location = \"%s\"\n" + "insecure = true\n", registryAsString); TestUtils.createRegistriesConfFile(homeDir1, config); // Return data from wiremock @@ -407,7 +400,7 @@ void shouldListTagsWithConfig(WireMockRuntimeInfo wmRuntimeInfo) throws Exceptio Registry registry = Registry.Builder.builder().withAuthProvider(authProvider).build(); List tags = registry.getTags( - ContainerRef.parse("%s/library/artifact-text-with-confg".formatted(registryAsString))) + ContainerRef.parse(String.format("%s/library/artifact-text-with-confg", registryAsString))) .tags(); assertEquals(2, tags.size()); assertEquals("latest", tags.get(0)); @@ -415,7 +408,7 @@ void shouldListTagsWithConfig(WireMockRuntimeInfo wmRuntimeInfo) throws Exceptio // With limit tags = registry.getTags( - ContainerRef.parse("%s/library/artifact-text-with-confg".formatted(registryAsString)), + ContainerRef.parse(String.format("%s/library/artifact-text-with-confg", registryAsString)), 1, null) .tags(); @@ -457,13 +450,7 @@ void shouldListRepositoryWithLocationConfig(WireMockRuntimeInfo wmRuntimeInfo) t String registryAsString = wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""); // language=toml - String config = - """ - [[registry]] - location = "%s" - insecure = true - """ - .formatted(registryAsString); + String config = String.format("[[registry]]\n" + "location = \"%s\"\n" + "insecure = true\n", registryAsString); TestUtils.createRegistriesConfFile(homeDir2, config); // Return data from wiremock @@ -492,13 +479,7 @@ void shouldListRepositoryWithPrefixConfig(WireMockRuntimeInfo wmRuntimeInfo) thr String registryAsString = wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""); // language=toml - String config = - """ - [[registry]] - prefix = "%s" - insecure = true - """ - .formatted(registryAsString); + String config = String.format("[[registry]]\n" + "prefix = \"%s\"\n" + "insecure = true\n", registryAsString); TestUtils.createRegistriesConfFile(homeDir3, config); // Return data from wiremock @@ -525,17 +506,15 @@ void shouldListRepositoryWithPrefixConfig(WireMockRuntimeInfo wmRuntimeInfo) thr void shouldListTagsWithFileStoreAuth(WireMockRuntimeInfo wmRuntimeInfo) throws IOException { // Auth file for current registry - String authFile = - """ - { - "auths": { - "localhost:%d": { - "auth": "bXl1c2VyOm15cGFzcw==" - } - } - } - """ - .formatted(wmRuntimeInfo.getHttpPort()); + String authFile = String.format( + "{\n" + + " \"auths\": {\n" + + " \"localhost:%d\": {\n" + + " \"auth\": \"bXl1c2VyOm15cGFzcw==\"\n" + + " }\n" + + " }\n" + + "}\n", + wmRuntimeInfo.getHttpPort()); Files.writeString(configDir.resolve("config.json"), authFile, StandardCharsets.UTF_8); @@ -555,8 +534,9 @@ void shouldListTagsWithFileStoreAuth(WireMockRuntimeInfo wmRuntimeInfo) throws I .build(); // Test - List tags = registry.getTags(ContainerRef.parse("%s/library/artifact-text-store" - .formatted(wmRuntimeInfo.getHttpBaseUrl().replace("http://", "")))) + List tags = registry.getTags(ContainerRef.parse(String.format( + "%s/library/artifact-text-store", + wmRuntimeInfo.getHttpBaseUrl().replace("http://", "")))) .tags(); // Assert @@ -579,12 +559,12 @@ void shouldHandle500Error(WireMockRuntimeInfo wmRuntimeInfo) { Registry registry = Registry.Builder.builder().withInsecure(true).build(); // Now we should have a reference to container - ContainerRef ref = ContainerRef.parse("%s/library/error-artifact".formatted(registryUrl)); + ContainerRef ref = ContainerRef.parse(String.format("%s/library/error-artifact", registryUrl)); OrasException exception = assertThrows(OrasException.class, () -> registry.getTags(ref)); assertEquals(500, exception.getStatusCode()); - ContainerRef ref2 = ContainerRef.parse("%s/library/error-artifact@sha256:1234".formatted(registryUrl)); + ContainerRef ref2 = ContainerRef.parse(String.format("%s/library/error-artifact@sha256:1234", registryUrl)); OrasException exception2 = assertThrows(OrasException.class, () -> registry.fetchBlobDescriptor(ref2)); assertEquals(500, exception2.getStatusCode()); } @@ -596,7 +576,7 @@ void shouldHandleTimeout(WireMockRuntimeInfo wmRuntimeInfo) { String registryUrl = wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""); // Using here a unique container reference to avoid conflicts when running in parallel - ContainerRef ref = ContainerRef.parse("%s/library/timeout-artifact".formatted(registryUrl)); + ContainerRef ref = ContainerRef.parse(String.format("%s/library/timeout-artifact", registryUrl)); // We Set up the stub for the timeout scenario wireMock.register(WireMock.get(WireMock.urlEqualTo("/v2/library/timeout-artifact/tags/list")) @@ -635,7 +615,7 @@ void shouldRetryBlobUpload(WireMockRuntimeInfo wmRuntimeInfo) throws IOException Registry registry = Registry.Builder.builder().withInsecure(true).withRetryDelay(0).build(); - ContainerRef ref = ContainerRef.parse("%s/library/artifact-text".formatted(registryUrl)); + ContainerRef ref = ContainerRef.parse(String.format("%s/library/artifact-text", registryUrl)); Path testFile = configDir.resolve("test-data.temp"); Files.writeString(testFile, "Test Content"); @@ -679,13 +659,14 @@ void shouldRefreshExpiredToken(WireMockRuntimeInfo wmRuntimeInfo) { // Return data from wiremock WireMock wireMock = wmRuntimeInfo.getWireMock(); - wireMock.register(WireMock.any(WireMock.urlEqualTo("/v2/library/refresh-token/blobs/%s".formatted(digest))) + wireMock.register(WireMock.any(WireMock.urlEqualTo(String.format("/v2/library/refresh-token/blobs/%s", digest))) .inScenario("get token") .willReturn(WireMock.forbidden() .withHeader( Const.WWW_AUTHENTICATE_HEADER, - "Bearer realm=\"http://localhost:%d/token\",service=\"localhost\",scope=\"repository:library/refresh-token:pull\"" - .formatted(wmRuntimeInfo.getHttpPort())))); + String.format( + "Bearer realm=\"http://localhost:%d/token\",service=\"localhost\",scope=\"repository:library/refresh-token:pull\"", + wmRuntimeInfo.getHttpPort())))); // Return token wireMock.register(WireMock.any( @@ -696,7 +677,7 @@ void shouldRefreshExpiredToken(WireMockRuntimeInfo wmRuntimeInfo) { new HttpClient.TokenResponse("fake-token", "access-token", null, 300, ZonedDateTime.now()))))); // On the second call we return ok - wireMock.register(WireMock.any(WireMock.urlEqualTo("/v2/library/refresh-token/blobs/%s".formatted(digest))) + wireMock.register(WireMock.any(WireMock.urlEqualTo(String.format("/v2/library/refresh-token/blobs/%s", digest))) .inScenario("get token") .whenScenarioStateIs("get") .willReturn( @@ -709,7 +690,7 @@ void shouldRefreshExpiredToken(WireMockRuntimeInfo wmRuntimeInfo) { .build(); ContainerRef containerRef = - ContainerRef.parse("localhost:%d/library/refresh-token".formatted(wmRuntimeInfo.getHttpPort())); + ContainerRef.parse(String.format("localhost:%d/library/refresh-token", wmRuntimeInfo.getHttpPort())); byte[] blob = registry.getBlob(containerRef.withDigest(digest)); assertEquals("blob-data", new String(blob)); @@ -722,7 +703,7 @@ void shouldRefreshExpiredToken(WireMockRuntimeInfo wmRuntimeInfo) { Const.METRIC_TAG_SERVICE, "localhost", Const.METRIC_TAG_REALM, - "http://localhost:%d/token".formatted(wmRuntimeInfo.getHttpPort())) + String.format("http://localhost:%d/token", wmRuntimeInfo.getHttpPort())) .count(), "Token refresh counter should be 1 after one token refresh"); assertEquals( @@ -790,7 +771,7 @@ void shouldHandleRateLimitingResponse(WireMockRuntimeInfo wmRuntimeInfo) { .withInsecure(true) .build(); - ContainerRef ref = ContainerRef.parse("%s/library/rate-limited".formatted(registryUrl)); + ContainerRef ref = ContainerRef.parse(String.format("%s/library/rate-limited", registryUrl)); // Verify that a 429 status code is thrown as an OrasException OrasException exception = assertThrows(OrasException.class, () -> registry.getTags(ref)); @@ -805,19 +786,20 @@ void shouldFollowRedirectOnBlobFetch(WireMockRuntimeInfo wmRuntimeInfo) { String digest = SupportedAlgorithm.SHA256.digest("blob-data".getBytes()); // Setup WireMock to redirect to a new location - String redirectUrl = "http://%s/v2/library/redirect-blob/blobs/redirected/%s".formatted(registryUrl, digest); - wireMock.register(get(urlEqualTo("/v2/library/redirect-blob/blobs/%s".formatted(digest))) + String redirectUrl = + String.format("http://%s/v2/library/redirect-blob/blobs/redirected/%s", registryUrl, digest); + wireMock.register(get(urlEqualTo(String.format("/v2/library/redirect-blob/blobs/%s", digest))) .willReturn(aResponse().withStatus(307).withHeader("Location", redirectUrl))); // Setup WireMock to serve blob at redirected location - wireMock.register(get(urlEqualTo("/v2/library/redirect-blob/blobs/redirected/%s".formatted(digest))) + wireMock.register(get(urlEqualTo(String.format("/v2/library/redirect-blob/blobs/redirected/%s", digest))) .willReturn(aResponse() .withStatus(200) .withHeader(Const.DOCKER_CONTENT_DIGEST_HEADER, digest) .withBody("blob-data"))); // Setup HEAD request for validation - wireMock.register(head(urlEqualTo("/v2/library/redirect-blob/blobs/%s".formatted(digest))) + wireMock.register(head(urlEqualTo(String.format("/v2/library/redirect-blob/blobs/%s", digest))) .willReturn(aResponse().withStatus(200))); Registry registry = Registry.Builder.builder() @@ -825,7 +807,7 @@ void shouldFollowRedirectOnBlobFetch(WireMockRuntimeInfo wmRuntimeInfo) { .withInsecure(true) .build(); - ContainerRef containerRef = ContainerRef.parse("%s/library/redirect-blob".formatted(registryUrl)); + ContainerRef containerRef = ContainerRef.parse(String.format("%s/library/redirect-blob", registryUrl)); byte[] blob = registry.getBlob(containerRef.withDigest(digest)); assertEquals("blob-data", new String(blob)); } @@ -845,7 +827,7 @@ void shouldComputeSizeWhenGettingDescriptorIfNull(WireMockRuntimeInfo wmRuntimeI .withAuthProvider(authProvider) .withInsecure(true) .build(); - ContainerRef containerRef = ContainerRef.parse("%s/library/null-size".formatted(registryUrl)); + ContainerRef containerRef = ContainerRef.parse(String.format("%s/library/null-size", registryUrl)); Descriptor descriptor = registry.getDescriptor(containerRef); assertNotNull(descriptor, "Descriptor should not be null"); @@ -870,7 +852,7 @@ void shouldGetSizeFromHeaderWhenGettingDescriptor(WireMockRuntimeInfo wmRuntimeI .withAuthProvider(authProvider) .withInsecure(true) .build(); - ContainerRef containerRef = ContainerRef.parse("%s/library/header-size-size".formatted(registryUrl)); + ContainerRef containerRef = ContainerRef.parse(String.format("%s/library/header-size-size", registryUrl)); Descriptor descriptor = registry.getDescriptor(containerRef); assertNotNull(descriptor, "Descriptor should not be null"); @@ -882,7 +864,7 @@ void shouldValidateDockerContentDigestForUnknownAlgorithm(WireMockRuntimeInfo wm WireMock wireMock = wmRuntimeInfo.getWireMock(); String registryUrl = wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""); String digest = SupportedAlgorithm.SHA256.digest("blob-data".getBytes()); - wireMock.register(any(urlEqualTo("/v2/library/validate-digest/blobs/%s".formatted(digest))) + wireMock.register(any(urlEqualTo(String.format("/v2/library/validate-digest/blobs/%s", digest))) .willReturn(aResponse() .withStatus(200) .withBody("blob-data") @@ -893,7 +875,7 @@ void shouldValidateDockerContentDigestForUnknownAlgorithm(WireMockRuntimeInfo wm .withAuthProvider(authProvider) .withInsecure(true) .build(); - ContainerRef containerRef = ContainerRef.parse("%s/library/validate-digest".formatted(registryUrl)); + ContainerRef containerRef = ContainerRef.parse(String.format("%s/library/validate-digest", registryUrl)); OrasException e = assertThrows( OrasException.class, () -> registry.getBlob(containerRef.withDigest(digest)), @@ -906,7 +888,7 @@ void shouldValidateDockerContentDigestMismatch(WireMockRuntimeInfo wmRuntimeInfo WireMock wireMock = wmRuntimeInfo.getWireMock(); String registryUrl = wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""); String digest = SupportedAlgorithm.SHA256.digest("blob-data".getBytes()); - wireMock.register(any(urlEqualTo("/v2/library/validate-digest/blobs/%s".formatted(digest))) + wireMock.register(any(urlEqualTo(String.format("/v2/library/validate-digest/blobs/%s", digest))) .willReturn(aResponse() .withStatus(200) .withBody("blob-data") @@ -917,7 +899,7 @@ void shouldValidateDockerContentDigestMismatch(WireMockRuntimeInfo wmRuntimeInfo .withAuthProvider(authProvider) .withInsecure(true) .build(); - ContainerRef containerRef = ContainerRef.parse("%s/library/validate-digest".formatted(registryUrl)); + ContainerRef containerRef = ContainerRef.parse(String.format("%s/library/validate-digest", registryUrl)); OrasException e = assertThrows( OrasException.class, () -> registry.getBlob(containerRef.withDigest(digest)), @@ -939,7 +921,7 @@ void shouldNotValidateDockerContentDigestWhenProbingDescriptor(WireMockRuntimeIn .withAuthProvider(authProvider) .withInsecure(true) .build(); - ContainerRef containerRef = ContainerRef.parse("%s/library/validate-digest".formatted(registryUrl)); + ContainerRef containerRef = ContainerRef.parse(String.format("%s/library/validate-digest", registryUrl)); Descriptor descriptor = registry.probeDescriptor(containerRef); assertNotNull(descriptor, "Descriptor should not be null"); } @@ -952,19 +934,21 @@ void shouldFollowRedirectAfterRequestingToken(WireMockRuntimeInfo wmRuntimeInfo) String registryUrl = wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""); // Redirect to a fake other storage - String redirectUrl = "http://%s/storage/%s".formatted(registryUrl, digest); + String redirectUrl = String.format("http://%s/storage/%s", registryUrl, digest); WireMock wireMock = wmRuntimeInfo.getWireMock(); // First we need to authenticate - wireMock.register(WireMock.any(WireMock.urlEqualTo("/v2/library/get-first-token/blobs/%s".formatted(digest))) - .inScenario("redirect after token") - .willSetStateTo("auth requested") - .willReturn(WireMock.unauthorized() - .withHeader( - Const.WWW_AUTHENTICATE_HEADER, - "Bearer realm=\"http://localhost:%d/token\",service=\"localhost\",scope=\"repository:library/get-first-token:pull\"" - .formatted(wmRuntimeInfo.getHttpPort())))); + wireMock.register( + WireMock.any(WireMock.urlEqualTo(String.format("/v2/library/get-first-token/blobs/%s", digest))) + .inScenario("redirect after token") + .willSetStateTo("auth requested") + .willReturn(WireMock.unauthorized() + .withHeader( + Const.WWW_AUTHENTICATE_HEADER, + String.format( + "Bearer realm=\"http://localhost:%d/token\",service=\"localhost\",scope=\"repository:library/get-first-token:pull\"", + wmRuntimeInfo.getHttpPort())))); // Token is returned wireMock.register(WireMock.any( @@ -976,14 +960,15 @@ void shouldFollowRedirectAfterRequestingToken(WireMockRuntimeInfo wmRuntimeInfo) new HttpClient.TokenResponse("fake-token", "access-token", null, 300, ZonedDateTime.now()))))); // After getting token we get a redirect - wireMock.register(WireMock.any(WireMock.urlEqualTo("/v2/library/get-first-token/blobs/%s".formatted(digest))) - .inScenario("redirect after token") - .whenScenarioStateIs("got token") - .willSetStateTo("redirect") - .willReturn(WireMock.temporaryRedirect(redirectUrl))); + wireMock.register( + WireMock.any(WireMock.urlEqualTo(String.format("/v2/library/get-first-token/blobs/%s", digest))) + .inScenario("redirect after token") + .whenScenarioStateIs("got token") + .willSetStateTo("redirect") + .willReturn(WireMock.temporaryRedirect(redirectUrl))); // We finally get the blob - wireMock.register(WireMock.any(WireMock.urlEqualTo("/storage/%s".formatted(digest))) + wireMock.register(WireMock.any(WireMock.urlEqualTo(String.format("/storage/%s", digest))) .inScenario("redirect after token") .whenScenarioStateIs("redirect") .willSetStateTo("done") @@ -995,7 +980,7 @@ void shouldFollowRedirectAfterRequestingToken(WireMockRuntimeInfo wmRuntimeInfo) .withInsecure(true) .build(); ContainerRef containerRef = - ContainerRef.parse("localhost:%d/library/get-first-token".formatted(wmRuntimeInfo.getHttpPort())); + ContainerRef.parse(String.format("localhost:%d/library/get-first-token", wmRuntimeInfo.getHttpPort())); byte[] blob = registry.getBlob(containerRef.withDigest(digest)); assertEquals("blob-data", new String(blob)); } @@ -1019,7 +1004,8 @@ void shouldHandleConcurrentBlobPushes(WireMockRuntimeInfo wmRuntimeInfo) throws .withInsecure(true) .build(); - ContainerRef containerRef = ContainerRef.parse("%s/library/concurrent-blob@%s".formatted(registryUrl, digest)); + ContainerRef containerRef = + ContainerRef.parse(String.format("%s/library/concurrent-blob@%s", registryUrl, digest)); // Create a temporary file for pushing Path testFile = configDir.resolve("concurrent-data.temp"); @@ -1056,7 +1042,7 @@ void shouldHandleNetworkConnectivityLoss(WireMockRuntimeInfo wmRuntimeInfo) { .withInsecure(true) .build(); - ContainerRef ref = ContainerRef.parse("%s/library/network-loss".formatted(registryUrl)); + ContainerRef ref = ContainerRef.parse(String.format("%s/library/network-loss", registryUrl)); // Verify that a network connectivity loss results in an OrasException OrasException exception = assertThrows(OrasException.class, () -> registry.getTags(ref)); @@ -1070,9 +1056,9 @@ void shouldHandleCorruptedResponse(WireMockRuntimeInfo wmRuntimeInfo) { String digest = SupportedAlgorithm.SHA256.digest("blob-data".getBytes()); // Setup WireMock to return a corrupted blob response - wireMock.register(head(urlEqualTo("/v2/library/corrupted-blob/blobs/%s".formatted(digest))) + wireMock.register(head(urlEqualTo(String.format("/v2/library/corrupted-blob/blobs/%s", digest))) .willReturn(aResponse().withStatus(200))); - wireMock.register(get(urlEqualTo("/v2/library/corrupted-blob/blobs/%s".formatted(digest))) + wireMock.register(get(urlEqualTo(String.format("/v2/library/corrupted-blob/blobs/%s", digest))) .willReturn(aResponse() .withStatus(200) .withHeader(Const.DOCKER_CONTENT_DIGEST_HEADER, digest) @@ -1083,7 +1069,7 @@ void shouldHandleCorruptedResponse(WireMockRuntimeInfo wmRuntimeInfo) { .withInsecure(true) .build(); - ContainerRef containerRef = ContainerRef.parse("%s/library/corrupted-blob".formatted(registryUrl)); + ContainerRef containerRef = ContainerRef.parse(String.format("%s/library/corrupted-blob", registryUrl)); // Expect digest mismatch exception OrasException exception = @@ -1099,24 +1085,27 @@ private byte[] tokenScenario( // Return data from wiremock WireMock wireMock = wmRuntimeInfo.getWireMock(); - wireMock.register(WireMock.any(WireMock.urlEqualTo("/v2/library/%s/blobs/%s".formatted(registryName, digest))) - .inScenario(registryName) - .willReturn(WireMock.unauthorized() - .withHeader( - Const.WWW_AUTHENTICATE_HEADER, - "Bearer realm=\"http://localhost:%d/token\",service=\"localhost\",scope=\"repository:library/%s:pull\"" - .formatted(wmRuntimeInfo.getHttpPort(), registryName)))); + wireMock.register( + WireMock.any(WireMock.urlEqualTo(String.format("/v2/library/%s/blobs/%s", registryName, digest))) + .inScenario(registryName) + .willReturn(WireMock.unauthorized() + .withHeader( + Const.WWW_AUTHENTICATE_HEADER, + String.format( + "Bearer realm=\"http://localhost:%d/token\",service=\"localhost\",scope=\"repository:library/%s:pull\"", + wmRuntimeInfo.getHttpPort(), registryName)))); // Return token wireMock.register(WireMock.any(WireMock.urlEqualTo( - "/token?scope=repository:library/%s:pull&service=localhost".formatted(registryName))) + String.format("/token?scope=repository:library/%s:pull&service=localhost", registryName))) .inScenario(registryName) .willSetStateTo("get") .willReturn(WireMock.okJson(JsonUtils.toJson( new HttpClient.TokenResponse(token, accessToken, null, 300, ZonedDateTime.now()))))); // On the second call we return ok - wireMock.register(WireMock.any(WireMock.urlEqualTo("/v2/library/%s/blobs/%s".formatted(registryName, digest))) + wireMock.register(WireMock.any( + WireMock.urlEqualTo(String.format("/v2/library/%s/blobs/%s", registryName, digest))) .inScenario(registryName) .whenScenarioStateIs("get") .willReturn( @@ -1129,7 +1118,7 @@ private byte[] tokenScenario( .build(); ContainerRef containerRef = - ContainerRef.parse("localhost:%d/library/%s".formatted(wmRuntimeInfo.getHttpPort(), registryName)); + ContainerRef.parse(String.format("localhost:%d/library/%s", wmRuntimeInfo.getHttpPort(), registryName)); return registry.getBlob(containerRef.withDigest(digest)); } @@ -1166,7 +1155,7 @@ void pullArtifactShouldRejectInvalidTitleAnnotation(WireMockRuntimeInfo wmRuntim .withBody(manifestJson))); // Stub GET blob - wireMock.register(get(urlEqualTo("/v2/library/malicious-artifact/blobs/%s".formatted(blobDigest))) + wireMock.register(get(urlEqualTo(String.format("/v2/library/malicious-artifact/blobs/%s", blobDigest))) .willReturn(aResponse() .withStatus(200) .withBody(blobContent) @@ -1177,7 +1166,8 @@ void pullArtifactShouldRejectInvalidTitleAnnotation(WireMockRuntimeInfo wmRuntim .withInsecure(true) .build(); - ContainerRef containerRef = ContainerRef.parse("%s/library/malicious-artifact:latest".formatted(registryUrl)); + ContainerRef containerRef = + ContainerRef.parse(String.format("%s/library/malicious-artifact:latest", registryUrl)); Path outputDir = configDir.resolve("pull-output"); Files.createDirectories(outputDir); @@ -1232,7 +1222,7 @@ void shouldRetryOn429WithRetryAfterHeader(WireMockRuntimeInfo wmRuntimeInfo) { .withMaxRetries(2) .build(); - ContainerRef ref = ContainerRef.parse("%s/library/rate-limited-retry".formatted(registryUrl)); + ContainerRef ref = ContainerRef.parse(String.format("%s/library/rate-limited-retry", registryUrl)); Tags tags = registry.getTags(ref); assertEquals(List.of("latest"), tags.tags()); @@ -1269,7 +1259,7 @@ void shouldRetryOn429WithExponentialBackoff(WireMockRuntimeInfo wmRuntimeInfo) { .withMaxRetries(3) .build(); - ContainerRef ref = ContainerRef.parse("%s/library/rate-limited-backoff".formatted(registryUrl)); + ContainerRef ref = ContainerRef.parse(String.format("%s/library/rate-limited-backoff", registryUrl)); Tags tags = registry.getTags(ref); assertEquals(List.of("v1"), tags.tags()); @@ -1299,7 +1289,7 @@ void shouldRetryOnNetworkError(WireMockRuntimeInfo wmRuntimeInfo) { .withMaxRetries(2) .build(); - ContainerRef ref = ContainerRef.parse("%s/library/network-error-retry".formatted(registryUrl)); + ContainerRef ref = ContainerRef.parse(String.format("%s/library/network-error-retry", registryUrl)); Tags tags = registry.getTags(ref); assertEquals(List.of("latest"), tags.tags()); @@ -1320,7 +1310,7 @@ void shouldNotRetryOnClientError(WireMockRuntimeInfo wmRuntimeInfo) { .withMaxRetries(3) .build(); - ContainerRef ref = ContainerRef.parse("%s/library/not-found".formatted(registryUrl)); + ContainerRef ref = ContainerRef.parse(String.format("%s/library/not-found", registryUrl)); OrasException exception = assertThrows(OrasException.class, () -> registry.getTags(ref)); assertEquals(404, exception.getStatusCode()); @@ -1343,7 +1333,7 @@ void shouldExhaustRetriesAndThrow(WireMockRuntimeInfo wmRuntimeInfo) { .withMaxRetries(3) .build(); - ContainerRef ref = ContainerRef.parse("%s/library/always-fails".formatted(registryUrl)); + ContainerRef ref = ContainerRef.parse(String.format("%s/library/always-fails", registryUrl)); assertThrows(OrasException.class, () -> registry.getTags(ref)); // 3 attempts total (initial + 2 retries) @@ -1357,12 +1347,13 @@ void shouldNotRetryTokenRefreshRequest(WireMockRuntimeInfo wmRuntimeInfo) { String digest = SupportedAlgorithm.SHA256.digest("blob-data".getBytes()); // Registry returns 401 to trigger token refresh - wireMock.register(get(urlPathEqualTo("/v2/library/no-retry-token/blobs/%s".formatted(digest))) + wireMock.register(get(urlPathEqualTo(String.format("/v2/library/no-retry-token/blobs/%s", digest))) .willReturn(forbidden() .withHeader( Const.WWW_AUTHENTICATE_HEADER, - "Bearer realm=\"http://localhost:%d/token\",service=\"localhost\",scope=\"repository:library/no-retry-token:pull\"" - .formatted(wmRuntimeInfo.getHttpPort())))); + String.format( + "Bearer realm=\"http://localhost:%d/token\",service=\"localhost\",scope=\"repository:library/no-retry-token:pull\"", + wmRuntimeInfo.getHttpPort())))); // Token endpoint always fails with 500 wireMock.register(get(urlPathEqualTo("/token")) @@ -1375,7 +1366,7 @@ void shouldNotRetryTokenRefreshRequest(WireMockRuntimeInfo wmRuntimeInfo) { .build(); ContainerRef ref = - ContainerRef.parse("localhost:%d/library/no-retry-token".formatted(wmRuntimeInfo.getHttpPort())); + ContainerRef.parse(String.format("localhost:%d/library/no-retry-token", wmRuntimeInfo.getHttpPort())); assertThrows(OrasException.class, () -> registry.getBlob(ref.withDigest(digest))); // Token endpoint must be called exactly once — no retry on token refresh @@ -1406,7 +1397,7 @@ void shouldFailChunkedUploadWhenInitiationReturnsNon202(WireMockRuntimeInfo wmRu // Path overload Path blobFile = configDir.resolve("chunked-init-error.txt"); Files.write(blobFile, content); - ContainerRef refPath = ContainerRef.parse("%s/library/chunked-init-error".formatted(registryUrl)); + ContainerRef refPath = ContainerRef.parse(String.format("%s/library/chunked-init-error", registryUrl)); OrasException exPath = assertThrows(OrasException.class, () -> registry.pushBlobChunked(refPath, blobFile, 4L)); assertEquals( @@ -1415,7 +1406,7 @@ void shouldFailChunkedUploadWhenInitiationReturnsNon202(WireMockRuntimeInfo wmRu "Exception message should include the unexpected status code"); // InputStream overload - ContainerRef refStream = ContainerRef.parse("%s/library/chunked-init-error".formatted(registryUrl)) + ContainerRef refStream = ContainerRef.parse(String.format("%s/library/chunked-init-error", registryUrl)) .withDigest(digest); OrasException exStream = assertThrows( @@ -1453,9 +1444,10 @@ void shouldFailCopyWhenIndexNestingExceedsMaxDepth(WireMockRuntimeInfo wmRuntime .withInsecure(true) .build(); - ContainerRef source = ContainerRef.parse("localhost:%d/%s:chain".formatted(wmRuntimeInfo.getHttpPort(), repo)); + ContainerRef source = + ContainerRef.parse(String.format("localhost:%d/%s:chain", wmRuntimeInfo.getHttpPort(), repo)); ContainerRef target = ContainerRef.parse( - "localhost:%d/library/deep-chain-target:copy".formatted(wmRuntimeInfo.getHttpPort())); + String.format("localhost:%d/library/deep-chain-target:copy", wmRuntimeInfo.getHttpPort())); OrasException e = assertThrows( OrasException.class, @@ -1476,27 +1468,22 @@ void shouldCopyWithoutLoopingOnCyclicIndexGraph(WireMockRuntimeInfo wmRuntimeInf // Reference itself stubNestedSourceIndex(wireMock, srcRepo, "self", digestA, digestA); - wireMock.register(WireMock.head(WireMock.urlEqualTo("/v2/%s/manifests/%s".formatted(srcRepo, digestA))) + wireMock.register(WireMock.head(WireMock.urlEqualTo(String.format("/v2/%s/manifests/%s", srcRepo, digestA))) .willReturn(WireMock.ok() .withHeader(Const.CONTENT_TYPE_HEADER, Const.DEFAULT_INDEX_MEDIA_TYPE) .withHeader(Const.DOCKER_CONTENT_DIGEST_HEADER, digestA))); // Generic target: accept any manifest push and return an empty index on the follow-up read. - String emptyIndex = - """ - { - "schemaVersion": 2, - "mediaType": "%s", - "manifests": [] - }""" - .formatted(Const.DEFAULT_INDEX_MEDIA_TYPE); - wireMock.register(WireMock.put(WireMock.urlMatching("/v2/%s/manifests/.*".formatted(dstRepo))) + String emptyIndex = String.format( + "{\n" + " \"schemaVersion\": 2,\n" + " \"mediaType\": \"%s\",\n" + " \"manifests\": []\n" + "}", + Const.DEFAULT_INDEX_MEDIA_TYPE); + wireMock.register(WireMock.put(WireMock.urlMatching(String.format("/v2/%s/manifests/.*", dstRepo))) .willReturn(WireMock.created().withHeader(Const.DOCKER_CONTENT_DIGEST_HEADER, digestA))); - wireMock.register(WireMock.head(WireMock.urlMatching("/v2/%s/manifests/.*".formatted(dstRepo))) + wireMock.register(WireMock.head(WireMock.urlMatching(String.format("/v2/%s/manifests/.*", dstRepo))) .willReturn(WireMock.ok() .withHeader(Const.CONTENT_TYPE_HEADER, Const.DEFAULT_INDEX_MEDIA_TYPE) .withHeader(Const.DOCKER_CONTENT_DIGEST_HEADER, digestA))); - wireMock.register(WireMock.get(WireMock.urlMatching("/v2/%s/manifests/.*".formatted(dstRepo))) + wireMock.register(WireMock.get(WireMock.urlMatching(String.format("/v2/%s/manifests/.*", dstRepo))) .willReturn(WireMock.ok() .withHeader(Const.CONTENT_TYPE_HEADER, Const.DEFAULT_INDEX_MEDIA_TYPE) .withHeader(Const.DOCKER_CONTENT_DIGEST_HEADER, digestA) @@ -1508,37 +1495,38 @@ void shouldCopyWithoutLoopingOnCyclicIndexGraph(WireMockRuntimeInfo wmRuntimeInf .build(); ContainerRef source = - ContainerRef.parse("localhost:%d/%s:self".formatted(wmRuntimeInfo.getHttpPort(), srcRepo)); + ContainerRef.parse(String.format("localhost:%d/%s:self", wmRuntimeInfo.getHttpPort(), srcRepo)); ContainerRef target = - ContainerRef.parse("localhost:%d/%s:copy".formatted(wmRuntimeInfo.getHttpPort(), dstRepo)); + ContainerRef.parse(String.format("localhost:%d/%s:copy", wmRuntimeInfo.getHttpPort(), dstRepo)); // Terminates rather than looping / overflowing. CopyUtils.copy(registry, source, registry, target, CopyUtils.CopyOptions.deep()); // The cycle guard returned before getIndex wireMock.verifyThat( - 0, WireMock.getRequestedFor(WireMock.urlEqualTo("/v2/%s/manifests/%s".formatted(srcRepo, digestA)))); + 0, + WireMock.getRequestedFor(WireMock.urlEqualTo(String.format("/v2/%s/manifests/%s", srcRepo, digestA)))); wireMock.verifyThat( - WireMock.headRequestedFor(WireMock.urlEqualTo("/v2/%s/manifests/%s".formatted(srcRepo, digestA)))); + WireMock.headRequestedFor(WireMock.urlEqualTo(String.format("/v2/%s/manifests/%s", srcRepo, digestA)))); } /** * Build the JSON of an index whose single entry is another index (the child). */ private static String nestedIndexJson(String childDigest) { - return """ - { - "schemaVersion": 2, - "mediaType": "%s", - "manifests": [ - { - "mediaType": "%s", - "digest": "%s", - "size": 100 - } - ] - }""" - .formatted(Const.DEFAULT_INDEX_MEDIA_TYPE, Const.DEFAULT_INDEX_MEDIA_TYPE, childDigest); + return String.format( + "{\n" + + " \"schemaVersion\": 2,\n" + + " \"mediaType\": \"%s\",\n" + + " \"manifests\": [\n" + + " {\n" + + " \"mediaType\": \"%s\",\n" + + " \"digest\": \"%s\",\n" + + " \"size\": 100\n" + + " }\n" + + " ]\n" + + "}", + Const.DEFAULT_INDEX_MEDIA_TYPE, Const.DEFAULT_INDEX_MEDIA_TYPE, childDigest); } /** @@ -1546,7 +1534,7 @@ private static String nestedIndexJson(String childDigest) { */ private static void stubNestedSourceIndex( WireMock wireMock, String repo, String ref, String selfDigest, String childDigest) { - String url = "/v2/%s/manifests/%s".formatted(repo, ref); + String url = String.format("/v2/%s/manifests/%s", repo, ref); wireMock.register(WireMock.head(WireMock.urlEqualTo(url)) .willReturn(WireMock.ok() .withHeader(Const.CONTENT_TYPE_HEADER, Const.DEFAULT_INDEX_MEDIA_TYPE) @@ -1565,16 +1553,18 @@ void shouldRejectBlobWhenContentDoesNotMatchPinnedDigest(WireMockRuntimeInfo wmR String selfConsistentHeader = SupportedAlgorithm.SHA256.digest(evil.getBytes(StandardCharsets.UTF_8)); WireMock wireMock = wmRuntimeInfo.getWireMock(); - wireMock.register(WireMock.get(WireMock.urlEqualTo("/v2/library/evil-blob/blobs/%s".formatted(pinnedDigest))) - .willReturn(WireMock.ok() - .withBody(evil) - .withHeader(Const.DOCKER_CONTENT_DIGEST_HEADER, selfConsistentHeader))); + wireMock.register( + WireMock.get(WireMock.urlEqualTo(String.format("/v2/library/evil-blob/blobs/%s", pinnedDigest))) + .willReturn(WireMock.ok() + .withBody(evil) + .withHeader(Const.DOCKER_CONTENT_DIGEST_HEADER, selfConsistentHeader))); Registry registry = Registry.Builder.builder() .withAuthProvider(authProvider) .withInsecure(true) .build(); - ContainerRef ref = ContainerRef.parse("localhost:%d/library/evil-blob".formatted(wmRuntimeInfo.getHttpPort())) + ContainerRef ref = ContainerRef.parse( + String.format("localhost:%d/library/evil-blob", wmRuntimeInfo.getHttpPort())) .withDigest(pinnedDigest); OrasException ex = assertThrows(OrasException.class, () -> registry.getBlob(ref)); @@ -1587,14 +1577,15 @@ void shouldReturnBinaryBlobWithoutCorruption(WireMockRuntimeInfo wmRuntimeInfo) String digest = SupportedAlgorithm.SHA256.digest(binary); WireMock wireMock = wmRuntimeInfo.getWireMock(); - wireMock.register(WireMock.get(WireMock.urlEqualTo("/v2/library/bin-blob/blobs/%s".formatted(digest))) + wireMock.register(WireMock.get(WireMock.urlEqualTo(String.format("/v2/library/bin-blob/blobs/%s", digest))) .willReturn(WireMock.ok().withBody(binary).withHeader(Const.DOCKER_CONTENT_DIGEST_HEADER, digest))); Registry registry = Registry.Builder.builder() .withAuthProvider(authProvider) .withInsecure(true) .build(); - ContainerRef ref = ContainerRef.parse("localhost:%d/library/bin-blob".formatted(wmRuntimeInfo.getHttpPort())) + ContainerRef ref = ContainerRef.parse( + String.format("localhost:%d/library/bin-blob", wmRuntimeInfo.getHttpPort())) .withDigest(digest); assertArrayEquals(binary, registry.getBlob(ref)); @@ -1605,14 +1596,16 @@ void shouldRejectFetchBlobToPathOnDigestMismatch(WireMockRuntimeInfo wmRuntimeIn String pinnedDigest = SupportedAlgorithm.SHA256.digest("good".getBytes(StandardCharsets.UTF_8)); WireMock wireMock = wmRuntimeInfo.getWireMock(); - wireMock.register(WireMock.get(WireMock.urlEqualTo("/v2/library/evil-file/blobs/%s".formatted(pinnedDigest))) - .willReturn(WireMock.ok().withBody("tampered"))); + wireMock.register( + WireMock.get(WireMock.urlEqualTo(String.format("/v2/library/evil-file/blobs/%s", pinnedDigest))) + .willReturn(WireMock.ok().withBody("tampered"))); Registry registry = Registry.Builder.builder() .withAuthProvider(authProvider) .withInsecure(true) .build(); - ContainerRef ref = ContainerRef.parse("localhost:%d/library/evil-file".formatted(wmRuntimeInfo.getHttpPort())) + ContainerRef ref = ContainerRef.parse( + String.format("localhost:%d/library/evil-file", wmRuntimeInfo.getHttpPort())) .withDigest(pinnedDigest); Path out = configDir.resolve("blob-out.bin"); @@ -1625,14 +1618,16 @@ void shouldRejectBlobStreamOnDigestMismatch(WireMockRuntimeInfo wmRuntimeInfo) { String pinnedDigest = SupportedAlgorithm.SHA256.digest("good".getBytes(StandardCharsets.UTF_8)); WireMock wireMock = wmRuntimeInfo.getWireMock(); - wireMock.register(WireMock.get(WireMock.urlEqualTo("/v2/library/evil-stream/blobs/%s".formatted(pinnedDigest))) - .willReturn(WireMock.ok().withBody("tampered"))); + wireMock.register( + WireMock.get(WireMock.urlEqualTo(String.format("/v2/library/evil-stream/blobs/%s", pinnedDigest))) + .willReturn(WireMock.ok().withBody("tampered"))); Registry registry = Registry.Builder.builder() .withAuthProvider(authProvider) .withInsecure(true) .build(); - ContainerRef ref = ContainerRef.parse("localhost:%d/library/evil-stream".formatted(wmRuntimeInfo.getHttpPort())) + ContainerRef ref = ContainerRef.parse( + String.format("localhost:%d/library/evil-stream", wmRuntimeInfo.getHttpPort())) .withDigest(pinnedDigest); // The digest is verified incrementally, so the mismatch surfaces when the stream is read to EOF. @@ -1647,17 +1642,13 @@ void shouldRejectBlobStreamOnDigestMismatch(WireMockRuntimeInfo wmRuntimeInfo) { @Test void shouldRejectManifestWhoseContentDoesNotMatchPinnedDigest(WireMockRuntimeInfo wmRuntimeInfo) { String realManifest = - """ - {"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json",\ - "config":{"mediaType":"application/vnd.oci.empty.v1+json",\ - "digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2},\ - "layers":[]}"""; + "{\"schemaVersion\":2,\"mediaType\":\"application/vnd.oci.image.manifest.v1+json\",\"config\":{\"mediaType\":\"application/vnd.oci.empty.v1+json\",\"digest\":\"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a\",\"size\":2},\"layers\":[]}"; String evilManifest = realManifest.replace("\"layers\":[]", "\"layers\":[],\"annotations\":{\"x\":\"y\"}"); String pinnedDigest = SupportedAlgorithm.SHA256.digest(realManifest.getBytes(StandardCharsets.UTF_8)); String selfConsistentHeader = SupportedAlgorithm.SHA256.digest(evilManifest.getBytes(StandardCharsets.UTF_8)); WireMock wireMock = wmRuntimeInfo.getWireMock(); - String manifestPath = "/v2/library/evil-manifest/manifests/%s".formatted(pinnedDigest); + String manifestPath = String.format("/v2/library/evil-manifest/manifests/%s", pinnedDigest); wireMock.register(WireMock.head(WireMock.urlEqualTo(manifestPath)) .willReturn(WireMock.aResponse() .withStatus(200) @@ -1675,7 +1666,7 @@ void shouldRejectManifestWhoseContentDoesNotMatchPinnedDigest(WireMockRuntimeInf .withInsecure(true) .build(); ContainerRef ref = ContainerRef.parse( - "localhost:%d/library/evil-manifest".formatted(wmRuntimeInfo.getHttpPort())) + String.format("localhost:%d/library/evil-manifest", wmRuntimeInfo.getHttpPort())) .withDigest(pinnedDigest); OrasException ex = assertThrows(OrasException.class, () -> registry.getManifest(ref)); @@ -1709,8 +1700,9 @@ void shouldFailOnTagListPaginationExceedingMaxPages(WireMockRuntimeInfo wmRuntim OrasException exception = assertThrows( OrasException.class, - () -> registry.getTags(ContainerRef.parse("%s/library/artifact-text" - .formatted(wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""))))); + () -> registry.getTags(ContainerRef.parse(String.format( + "%s/library/artifact-text", + wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""))))); assertTrue( exception.getMessage().contains("Tag listing exceeded 2 pages"), "Unexpected message: " + exception.getMessage()); @@ -1724,15 +1716,9 @@ void shouldFailOnReferrerListPaginationExceedingMaxPages(WireMockRuntimeInfo wmR // Minimal referrers index JSON with one manifest entry per page String page1 = - """ - {"mediaType":"application/vnd.oci.image.index.v1+json","manifests":\ - [{"mediaType":"application/vnd.oci.image.manifest.v1+json",\ - "digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","size":1}]}"""; + "{\"mediaType\":\"application/vnd.oci.image.index.v1+json\",\"manifests\":[{\"mediaType\":\"application/vnd.oci.image.manifest.v1+json\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"size\":1}]}"; String page2 = - """ - {"mediaType":"application/vnd.oci.image.index.v1+json","manifests":\ - [{"mediaType":"application/vnd.oci.image.manifest.v1+json",\ - "digest":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","size":1}]}"""; + "{\"mediaType\":\"application/vnd.oci.image.index.v1+json\",\"manifests\":[{\"mediaType\":\"application/vnd.oci.image.manifest.v1+json\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1}]}"; WireMock wireMock = wmRuntimeInfo.getWireMock(); wireMock.register(WireMock.get(WireMock.urlPathEqualTo(referrersPath)) @@ -1756,8 +1742,9 @@ void shouldFailOnReferrerListPaginationExceedingMaxPages(WireMockRuntimeInfo wmR .withReferrerListMaxPages(2) .build(); - ContainerRef ref = ContainerRef.parse("%s/library/artifact-text" - .formatted(wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""))) + ContainerRef ref = ContainerRef.parse(String.format( + "%s/library/artifact-text", + wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""))) .withDigest(digest); OrasException exception = assertThrows(OrasException.class, () -> registry.getReferrers(ref, null)); @@ -1775,11 +1762,7 @@ void shouldFallbackToReferrersTagSchemaWhenReferrersApiUnavailable(WireMockRunti + "sha256-44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"; String fallbackIndex = - """ - {"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":\ - [{"mediaType":"application/vnd.oci.image.manifest.v1+json",\ - "digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","size":1,\ - "artifactType":"application/vnd.example+json"}]}"""; + "{\"schemaVersion\":2,\"mediaType\":\"application/vnd.oci.image.index.v1+json\",\"manifests\":[{\"mediaType\":\"application/vnd.oci.image.manifest.v1+json\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"size\":1,\"artifactType\":\"application/vnd.example+json\"}]}"; WireMock wireMock = wmRuntimeInfo.getWireMock(); // Registry does not implement the Referrers API (e.g. GHCR) @@ -1796,8 +1779,9 @@ void shouldFallbackToReferrersTagSchemaWhenReferrersApiUnavailable(WireMockRunti .withInsecure(true) .build(); - ContainerRef ref = ContainerRef.parse("%s/library/artifact-text" - .formatted(wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""))) + ContainerRef ref = ContainerRef.parse(String.format( + "%s/library/artifact-text", + wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""))) .withDigest(digest); Referrers referrers = registry.getReferrers(ref, null); @@ -1816,14 +1800,7 @@ void shouldFilterFallbackReferrersByArtifactType(WireMockRuntimeInfo wmRuntimeIn + "sha256-44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"; String fallbackIndex = - """ - {"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":\ - [{"mediaType":"application/vnd.oci.image.manifest.v1+json",\ - "digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","size":1,\ - "artifactType":"application/vnd.example+json"},\ - {"mediaType":"application/vnd.oci.image.manifest.v1+json",\ - "digest":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","size":1,\ - "artifactType":"application/vnd.other+json"}]}"""; + "{\"schemaVersion\":2,\"mediaType\":\"application/vnd.oci.image.index.v1+json\",\"manifests\":[{\"mediaType\":\"application/vnd.oci.image.manifest.v1+json\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"size\":1,\"artifactType\":\"application/vnd.example+json\"},{\"mediaType\":\"application/vnd.oci.image.manifest.v1+json\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1,\"artifactType\":\"application/vnd.other+json\"}]}"; WireMock wireMock = wmRuntimeInfo.getWireMock(); wireMock.register(WireMock.get(WireMock.urlPathEqualTo(referrersPath)).willReturn(WireMock.notFound())); @@ -1838,8 +1815,9 @@ void shouldFilterFallbackReferrersByArtifactType(WireMockRuntimeInfo wmRuntimeIn .withInsecure(true) .build(); - ContainerRef ref = ContainerRef.parse("%s/library/artifact-text" - .formatted(wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""))) + ContainerRef ref = ContainerRef.parse(String.format( + "%s/library/artifact-text", + wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""))) .withDigest(digest); Referrers referrers = registry.getReferrers(ref, ArtifactType.from("application/vnd.example+json")); @@ -1866,8 +1844,9 @@ void shouldReturnEmptyReferrersWhenFallbackTagDoesNotExist(WireMockRuntimeInfo w .withInsecure(true) .build(); - ContainerRef ref = ContainerRef.parse("%s/library/artifact-text" - .formatted(wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""))) + ContainerRef ref = ContainerRef.parse(String.format( + "%s/library/artifact-text", + wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""))) .withDigest(digest); Referrers referrers = registry.getReferrers(ref, null); diff --git a/src/test/java/land/oras/SubjectTest.java b/src/test/java/land/oras/SubjectTest.java index 9efc8183..c2aa19f3 100644 --- a/src/test/java/land/oras/SubjectTest.java +++ b/src/test/java/land/oras/SubjectTest.java @@ -70,12 +70,10 @@ void testToString() { */ private String sampleSubject() { // language=JSON - return """ - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "digest": "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", - "size": 32654 - } - """; + return "{\n" + + " \"mediaType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n" + + " \"digest\": \"sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890\",\n" + + " \"size\": 32654\n" + + "}\n"; } } diff --git a/src/test/java/land/oras/auth/AuthStoreAuthenticationProviderTest.java b/src/test/java/land/oras/auth/AuthStoreAuthenticationProviderTest.java index dfde8673..439be68b 100644 --- a/src/test/java/land/oras/auth/AuthStoreAuthenticationProviderTest.java +++ b/src/test/java/land/oras/auth/AuthStoreAuthenticationProviderTest.java @@ -55,7 +55,7 @@ void testGetAuthHeader() throws Exception { AuthStoreAuthenticationProvider authProvider = new AuthStoreAuthenticationProvider(mockAuthStore); // Verify that the getAuthHeader method returns the expected Basic Auth header - String authHeader = authProvider.getAuthHeader(ContainerRef.parse("%s/%s".formatted(REGISTRY, "alpine"))); + String authHeader = authProvider.getAuthHeader(ContainerRef.parse(String.format("%s/%s", REGISTRY, "alpine"))); String expectedAuthString = "testUser:testPassword"; String expectedEncodedAuth = "Basic " + Base64.getEncoder().encodeToString(expectedAuthString.getBytes(StandardCharsets.UTF_8)); @@ -73,7 +73,7 @@ void testGetNullAuthHeader() throws Exception { AuthStoreAuthenticationProvider authProvider = new AuthStoreAuthenticationProvider(mockAuthStore); // Verify that the getAuthHeader method returns the expected Basic Auth header - String authHeader = authProvider.getAuthHeader(ContainerRef.parse("%s/%s".formatted(REGISTRY, "alpine"))); + String authHeader = authProvider.getAuthHeader(ContainerRef.parse(String.format("%s/%s", REGISTRY, "alpine"))); String expectedAuthString = "testUser:testPassword"; String expectedEncodedAuth = "Basic " + Base64.getEncoder().encodeToString(expectedAuthString.getBytes(StandardCharsets.UTF_8)); @@ -88,8 +88,8 @@ void testDefaultLocation() { @Test void identityShouldFollowTheResolvedCredentialPerRegistry() { - ContainerRef alpine = ContainerRef.parse("%s/%s".formatted(REGISTRY, "alpine")); - ContainerRef busybox = ContainerRef.parse("%s/%s".formatted(REGISTRY, "busybox")); + ContainerRef alpine = ContainerRef.parse(String.format("%s/%s", REGISTRY, "alpine")); + ContainerRef busybox = ContainerRef.parse(String.format("%s/%s", REGISTRY, "busybox")); doReturn(new Credential("alice", "alice-pass")).when(mockAuthStore).get(alpine); doReturn(new Credential("bob", "bob-pass")).when(mockAuthStore).get(busybox); @@ -107,6 +107,7 @@ void identityShouldFallBackToAnonymousWhenNoCredentialIsStored() { AuthStoreAuthenticationProvider authProvider = new AuthStoreAuthenticationProvider(mockAuthStore); assertEquals( - "BASIC:anonymous", authProvider.getIdentity(ContainerRef.parse("%s/%s".formatted(REGISTRY, "alpine")))); + "BASIC:anonymous", + authProvider.getIdentity(ContainerRef.parse(String.format("%s/%s", REGISTRY, "alpine")))); } } diff --git a/src/test/java/land/oras/auth/AuthStoreTest.java b/src/test/java/land/oras/auth/AuthStoreTest.java index 4d8fa29f..3b46fcf6 100644 --- a/src/test/java/land/oras/auth/AuthStoreTest.java +++ b/src/test/java/land/oras/auth/AuthStoreTest.java @@ -58,52 +58,46 @@ class AuthStoreTest { private static final String PASSWORD = "password"; // language=json - public static final String SAMPLE_DOCKER_CONFIG = - """ - { - "auths": { - "registry.example.com": { - "auth": "dXNlcjpwYXNzd29yZA==" - }, - "another.registry.com": { - "auth": "dXNlcjpwYXNzd29yZA==" - } - }, - "credsStore": "unknown", - "credHelpers": { - "registry.other.com": "foo-binary", - "another.other.com": "bar-binary", - "new.other.com": "pass", - "other.other.com": "other-binary", - "creds.other.com": "fake", - "error.other.com": "fake" - } - } - """; + public static final String SAMPLE_DOCKER_CONFIG = "{\n" + + " \"auths\": {\n" + + " \"registry.example.com\": {\n" + + " \"auth\": \"dXNlcjpwYXNzd29yZA==\"\n" + + " },\n" + + " \"another.registry.com\": {\n" + + " \"auth\": \"dXNlcjpwYXNzd29yZA==\"\n" + + " }\n" + + " },\n" + + " \"credsStore\": \"unknown\",\n" + + " \"credHelpers\": {\n" + + " \"registry.other.com\": \"foo-binary\",\n" + + " \"another.other.com\": \"bar-binary\",\n" + + " \"new.other.com\": \"pass\",\n" + + " \"other.other.com\": \"other-binary\",\n" + + " \"creds.other.com\": \"fake\",\n" + + " \"error.other.com\": \"fake\"\n" + + " }\n" + + "}\n"; // language=json - public static final String SAMPLE_PODMAN_CONFIG = - """ - { - "auths": { - "registry.other.com": { - "auth": "dXNlcjpwYXNzd29yZA==" - }, - "another.other.com": { - "auth": "dXNlcjpwYXNzd29yZA==" - } - }, - "credsStore": "unknown", - "credHelpers": { - "registry.other.com": "foo-binary", - "another.other.com": "bar-binary", - "new.other.com": "pass", - "other.other.com": "other-binary", - "creds.other.com": "fake", - "error.other.com": "fake" - } - } - """; + public static final String SAMPLE_PODMAN_CONFIG = "{\n" + + " \"auths\": {\n" + + " \"registry.other.com\": {\n" + + " \"auth\": \"dXNlcjpwYXNzd29yZA==\"\n" + + " },\n" + + " \"another.other.com\": {\n" + + " \"auth\": \"dXNlcjpwYXNzd29yZA==\"\n" + + " }\n" + + " },\n" + + " \"credsStore\": \"unknown\",\n" + + " \"credHelpers\": {\n" + + " \"registry.other.com\": \"foo-binary\",\n" + + " \"another.other.com\": \"bar-binary\",\n" + + " \"new.other.com\": \"pass\",\n" + + " \"other.other.com\": \"other-binary\",\n" + + " \"creds.other.com\": \"fake\",\n" + + " \"error.other.com\": \"fake\"\n" + + " }\n" + + "}\n"; @BeforeAll static void init() throws Exception { @@ -293,22 +287,19 @@ void testShouldReadCredentialsFromPodManConfig() throws Exception { } // language=json - public static final String SAMPLE_HIERARCHICAL_CONFIG = - """ - { - "auths": { - "my-registry.local/namespace/user/image": { - "auth": "dXNlcjE6cGFzczE=" - }, - "my-registry.local/namespace": { - "auth": "dXNlcjM6cGFzczM=" - }, - "my-registry.local": { - "auth": "dXNlcjI6cGFzczI=" - } - } - } - """; + public static final String SAMPLE_HIERARCHICAL_CONFIG = "{\n" + + " \"auths\": {\n" + + " \"my-registry.local/namespace/user/image\": {\n" + + " \"auth\": \"dXNlcjE6cGFzczE=\"\n" + + " },\n" + + " \"my-registry.local/namespace\": {\n" + + " \"auth\": \"dXNlcjM6cGFzczM=\"\n" + + " },\n" + + " \"my-registry.local\": {\n" + + " \"auth\": \"dXNlcjI6cGFzczI=\"\n" + + " }\n" + + " }\n" + + "}\n"; @Test void testHierarchicalCredentialLookupMostSpecific() throws Exception { @@ -412,16 +403,13 @@ void testRegistryAuthFileIsUsedWhenSet() throws Exception { @Test void testRegistryAuthFileTakesPrecedenceOverDefaults() throws Exception { // language=json - String customConfig = - """ - { - "auths": { - "custom.registry.com": { - "auth": "dXNlcjpwYXNzd29yZA==" - } - } - } - """; + String customConfig = "{\n" + + " \"auths\": {\n" + + " \"custom.registry.com\": {\n" + + " \"auth\": \"dXNlcjpwYXNzd29yZA==\"\n" + + " }\n" + + " }\n" + + "}\n"; Path authFile = tempDir.resolve("custom-auth.json"); Files.writeString(authFile, customConfig); @@ -489,15 +477,13 @@ void testPasswordContainingColonIsPreserved() throws Exception { String auth = java.util.Base64.getEncoder() .encodeToString((user + ":" + password).getBytes(java.nio.charset.StandardCharsets.UTF_8)); // language=json - String config = - """ - { - "auths": { - "colon.registry.com": { "auth": "%s" } - } - } - """ - .formatted(auth); + String config = String.format( + "{\n" + + " \"auths\": {\n" + + " \"colon.registry.com\": { \"auth\": \"%s\" }\n" + + " }\n" + + "}\n", + auth); Path configFile = tempDir.resolve("colon-config.json"); Files.writeString(configFile, config); @@ -516,16 +502,14 @@ void testMalformedEntryIsSkippedWithoutDroppingOtherCredentials() throws Excepti String valid = java.util.Base64.getEncoder() .encodeToString("user:password".getBytes(java.nio.charset.StandardCharsets.UTF_8)); // language=json - String config = - """ - { - "auths": { - "bad.registry.com": { "auth": "%s" }, - "good.registry.com": { "auth": "%s" } - } - } - """ - .formatted(malformed, valid); + String config = String.format( + "{\n" + + " \"auths\": {\n" + + " \"bad.registry.com\": { \"auth\": \"%s\" },\n" + + " \"good.registry.com\": { \"auth\": \"%s\" }\n" + + " }\n" + + "}\n", + malformed, valid); Path configFile = tempDir.resolve("malformed-config.json"); Files.writeString(configFile, config); @@ -542,15 +526,13 @@ void testEmptyPasswordIsPreserved() throws Exception { String auth = java.util.Base64.getEncoder().encodeToString("user:".getBytes(java.nio.charset.StandardCharsets.UTF_8)); // language=json - String config = - """ - { - "auths": { - "empty.registry.com": { "auth": "%s" } - } - } - """ - .formatted(auth); + String config = String.format( + "{\n" + + " \"auths\": {\n" + + " \"empty.registry.com\": { \"auth\": \"%s\" }\n" + + " }\n" + + "}\n", + auth); Path configFile = tempDir.resolve("empty-pass-config.json"); Files.writeString(configFile, config); @@ -569,15 +551,13 @@ void testPasswordWithArbitraryCharactersIsPreserved() throws Exception { String auth = java.util.Base64.getEncoder() .encodeToString((user + ":" + password).getBytes(java.nio.charset.StandardCharsets.UTF_8)); // language=json - String config = - """ - { - "auths": { - "any.registry.com": { "auth": "%s" } - } - } - """ - .formatted(auth); + String config = String.format( + "{\n" + + " \"auths\": {\n" + + " \"any.registry.com\": { \"auth\": \"%s\" }\n" + + " }\n" + + "}\n", + auth); Path configFile = tempDir.resolve("any-char-config.json"); Files.writeString(configFile, config); @@ -593,14 +573,9 @@ void testPasswordWithArbitraryCharactersIsPreserved() throws Exception { void shouldRejectCredentialHelperThatEscapesPrefix() throws Exception { String maliciousSuffix = "../../../../../../tmp/pwn"; // language=json - String config = - """ - { - "auths": {}, - "credHelpers": { "evil.registry.com": "%s" } - } - """ - .formatted(maliciousSuffix); + String config = String.format( + "{\n" + " \"auths\": {},\n" + " \"credHelpers\": { \"evil.registry.com\": \"%s\" }\n" + "}\n", + maliciousSuffix); Path configFile = tempDir.resolve("evil-helper-config.json"); Files.writeString(configFile, config); @@ -614,13 +589,10 @@ void shouldRejectCredentialHelperThatEscapesPrefix() throws Exception { @Test void shouldReturnValidCredentialHelperBinary() throws Exception { // language=json - String config = - """ - { - "auths": {}, - "credHelpers": { "good.registry.com": "ecr-login" } - } - """; + String config = "{\n" + + " \"auths\": {},\n" + + " \"credHelpers\": { \"good.registry.com\": \"ecr-login\" }\n" + + "}\n"; Path configFile = tempDir.resolve("good-helper-config.json"); Files.writeString(configFile, config); diff --git a/src/test/java/land/oras/auth/RegistriesConfTest.java b/src/test/java/land/oras/auth/RegistriesConfTest.java index e84cb996..f4a2131f 100644 --- a/src/test/java/land/oras/auth/RegistriesConfTest.java +++ b/src/test/java/land/oras/auth/RegistriesConfTest.java @@ -41,13 +41,10 @@ class RegistriesConfTest { private static Path homeDir; // language=toml - public static final String HOME_REGISTRIES_CONF = - """ - unqualified-search-registries = ["docker.io"] - - [aliases] - "alpine"="docker.io/library/alpine" - """; + public static final String HOME_REGISTRIES_CONF = "unqualified-search-registries = [\"docker.io\"]\n" + + "\n" + + "[aliases]\n" + + "\"alpine\"=\"docker.io/library/alpine\"\n"; @BeforeAll static void init() { @@ -81,12 +78,7 @@ void shouldLoadDropInConfFiles(@TempDir Path dropInHomeDir) throws Exception { TestUtils.createRegistriesConfFile(dropInHomeDir, "unqualified-search-registries = [\"docker.io\"]"); // language=toml TestUtils.createDropInConfFile( - dropInHomeDir, - "10-extra.conf", - """ - [aliases] - "myapp"="registry.example.com/myapp" - """); + dropInHomeDir, "10-extra.conf", "[aliases]\n" + "\"myapp\"=\"registry.example.com/myapp\"\n"); TestUtils.withHome(dropInHomeDir, () -> { RegistriesConf conf = RegistriesConf.newConf(); @@ -103,20 +95,10 @@ void shouldLoadDropInConfFilesInAlphaNumericalOrder(@TempDir Path dropInHomeDir) TestUtils.createRegistriesConfFile(dropInHomeDir, ""); // language=toml TestUtils.createDropInConfFile( - dropInHomeDir, - "01-first.conf", - """ - [aliases] - "foo"="registry.first.com/foo" - """); + dropInHomeDir, "01-first.conf", "[aliases]\n" + "\"foo\"=\"registry.first.com/foo\"\n"); // language=toml TestUtils.createDropInConfFile( - dropInHomeDir, - "02-second.conf", - """ - [aliases] - "foo"="registry.second.com/foo" - """); + dropInHomeDir, "02-second.conf", "[aliases]\n" + "\"foo\"=\"registry.second.com/foo\"\n"); TestUtils.withHome(dropInHomeDir, () -> { RegistriesConf conf = RegistriesConf.newConf(); diff --git a/src/test/java/land/oras/auth/RegistryConfTest.java b/src/test/java/land/oras/auth/RegistryConfTest.java index 12997250..eec1c78b 100644 --- a/src/test/java/land/oras/auth/RegistryConfTest.java +++ b/src/test/java/land/oras/auth/RegistryConfTest.java @@ -222,20 +222,17 @@ void shouldRewriteUnqualifiedContainerRef() { @Test void shouldParseMirrorsFromToml() { // language=toml - String toml = - """ - [[registry]] - prefix = "docker.io" - location = "docker.io" - - [[registry.mirror]] - location = "mirror1.example.com" - insecure = true - - [[registry.mirror]] - location = "mirror2.example.com" - insecure = false - """; + String toml = "[[registry]]\n" + + "prefix = \"docker.io\"\n" + + "location = \"docker.io\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"mirror1.example.com\"\n" + + "insecure = true\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"mirror2.example.com\"\n" + + "insecure = false\n"; RegistriesConf conf = RegistriesConf.newConf(List.of(writeTempToml(toml))); @@ -362,27 +359,24 @@ void shouldRewriteForMirrorWithTrailingSlash() { @Test void shouldParsePullFromMirrorFromToml() { // language=toml - String toml = - """ - [[registry]] - prefix = "docker.io" - location = "docker.io" - - [[registry.mirror]] - location = "mirror-digest.example.com" - pull-from-mirror = "digest-only" - - [[registry.mirror]] - location = "mirror-tag.example.com" - pull-from-mirror = "tag-only" - - [[registry.mirror]] - location = "mirror-all.example.com" - pull-from-mirror = "all" - - [[registry.mirror]] - location = "mirror-default.example.com" - """; + String toml = "[[registry]]\n" + + "prefix = \"docker.io\"\n" + + "location = \"docker.io\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"mirror-digest.example.com\"\n" + + "pull-from-mirror = \"digest-only\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"mirror-tag.example.com\"\n" + + "pull-from-mirror = \"tag-only\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"mirror-all.example.com\"\n" + + "pull-from-mirror = \"all\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"mirror-default.example.com\"\n"; RegistriesConf conf = RegistriesConf.newConf(List.of(writeTempToml(toml))); ContainerRef ref = ContainerRef.parse("docker.io/library/alpine:latest"); @@ -398,23 +392,20 @@ void shouldParsePullFromMirrorFromToml() { @Test void shouldFilterMirrorsByPullFromMirrorForTagRef() { // language=toml - String toml = - """ - [[registry]] - prefix = "docker.io" - location = "docker.io" - - [[registry.mirror]] - location = "mirror-digest.example.com" - pull-from-mirror = "digest-only" - - [[registry.mirror]] - location = "mirror-tag.example.com" - pull-from-mirror = "tag-only" - - [[registry.mirror]] - location = "mirror-all.example.com" - """; + String toml = "[[registry]]\n" + + "prefix = \"docker.io\"\n" + + "location = \"docker.io\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"mirror-digest.example.com\"\n" + + "pull-from-mirror = \"digest-only\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"mirror-tag.example.com\"\n" + + "pull-from-mirror = \"tag-only\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"mirror-all.example.com\"\n"; RegistriesConf conf = RegistriesConf.newConf(List.of(writeTempToml(toml))); @@ -430,23 +421,20 @@ void shouldFilterMirrorsByPullFromMirrorForTagRef() { @Test void shouldFilterMirrorsByPullFromMirrorForDigestRef() { // language=toml - String toml = - """ - [[registry]] - prefix = "docker.io" - location = "docker.io" - - [[registry.mirror]] - location = "mirror-digest.example.com" - pull-from-mirror = "digest-only" - - [[registry.mirror]] - location = "mirror-tag.example.com" - pull-from-mirror = "tag-only" - - [[registry.mirror]] - location = "mirror-all.example.com" - """; + String toml = "[[registry]]\n" + + "prefix = \"docker.io\"\n" + + "location = \"docker.io\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"mirror-digest.example.com\"\n" + + "pull-from-mirror = \"digest-only\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"mirror-tag.example.com\"\n" + + "pull-from-mirror = \"tag-only\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"mirror-all.example.com\"\n"; RegistriesConf conf = RegistriesConf.newConf(List.of(writeTempToml(toml))); @@ -463,20 +451,17 @@ void shouldFilterMirrorsByPullFromMirrorForDigestRef() { @Test void shouldApplyMirrorByDigestOnly() { // language=toml - String toml = - """ - [[registry]] - prefix = "docker.io" - location = "docker.io" - mirror-by-digest-only = true - - [[registry.mirror]] - location = "mirror-a.example.com" - - [[registry.mirror]] - location = "mirror-b.example.com" - pull-from-mirror = "tag-only" - """; + String toml = "[[registry]]\n" + + "prefix = \"docker.io\"\n" + + "location = \"docker.io\"\n" + + "mirror-by-digest-only = true\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"mirror-a.example.com\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"mirror-b.example.com\"\n" + + "pull-from-mirror = \"tag-only\"\n"; RegistriesConf conf = RegistriesConf.newConf(List.of(writeTempToml(toml))); @@ -496,18 +481,15 @@ void shouldApplyMirrorByDigestOnly() { @Test void shouldReturnAllMirrorsWhenNoFilterConfigured() { // language=toml - String toml = - """ - [[registry]] - prefix = "docker.io" - location = "docker.io" - - [[registry.mirror]] - location = "mirror-a.example.com" - - [[registry.mirror]] - location = "mirror-b.example.com" - """; + String toml = "[[registry]]\n" + + "prefix = \"docker.io\"\n" + + "location = \"docker.io\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"mirror-a.example.com\"\n" + + "\n" + + "[[registry.mirror]]\n" + + "location = \"mirror-b.example.com\"\n"; RegistriesConf conf = RegistriesConf.newConf(List.of(writeTempToml(toml))); ContainerRef ref = ContainerRef.parse("docker.io/library/alpine:latest"); diff --git a/src/test/java/land/oras/policy/ContainersPolicyTest.java b/src/test/java/land/oras/policy/ContainersPolicyTest.java index eca0ab4e..001b9605 100644 --- a/src/test/java/land/oras/policy/ContainersPolicyTest.java +++ b/src/test/java/land/oras/policy/ContainersPolicyTest.java @@ -164,9 +164,7 @@ void evaluatingSigstoreSignedPassesScopeGate() { @Test void loadsUserPolicyFromHome(@TempDir Path homeDir) throws Exception { // language=json - String policyJson = """ - {"default": [{"type": "insecureAcceptAnything"}]} - """; + String policyJson = "{\"default\": [{\"type\": \"insecureAcceptAnything\"}]}\n"; writePolicyFile(homeDir, policyJson); TestUtils.withHome(homeDir, () -> { @@ -278,10 +276,9 @@ void sigstoreSignedGlobalDefaultApplies(@TempDir Path dir) throws IOException { // language=json Files.writeString( path, - """ - {"default": [{"type": "sigstoreSigned", "keyData": "%s"}]} - """ - .formatted(SigstoreTestSupport.keyData(kp.getPublic()))); + String.format( + "{\"default\": [{\"type\": \"sigstoreSigned\", \"keyData\": \"%s\"}]}\n", + SigstoreTestSupport.keyData(kp.getPublic()))); ContainersPolicy policy = ContainersPolicy.newPolicy(path); assertDoesNotThrow(() -> policy.verify(context( @@ -360,22 +357,20 @@ void sigstoreSignedWithKeyPathsList(@TempDir Path dir) throws IOException { // language=json Files.writeString( path, - """ - { - "default": [{"type": "reject"}], - "transports": { - "docker": { - "registry.example.com/app": [{ - "type": "sigstoreSigned", - "keyPaths": ["%s", "%s"] - }] - } - } - } - """ - .formatted( - keyFile1.toString().replace("\\", "\\\\"), - keyFile2.toString().replace("\\", "\\\\"))); + String.format( + "{\n" + + " \"default\": [{\"type\": \"reject\"}],\n" + + " \"transports\": {\n" + + " \"docker\": {\n" + + " \"registry.example.com/app\": [{\n" + + " \"type\": \"sigstoreSigned\",\n" + + " \"keyPaths\": [\"%s\", \"%s\"]\n" + + " }]\n" + + " }\n" + + " }\n" + + "}\n", + keyFile1.toString().replace("\\", "\\\\"), + keyFile2.toString().replace("\\", "\\\\"))); ContainersPolicy policy = ContainersPolicy.newPolicy(path); // Accepted when signed by kp1 @@ -409,22 +404,19 @@ void sigstoreSignedWithKeyDatasList(@TempDir Path dir) throws IOException { // language=json Files.writeString( path, - """ - { - "default": [{"type": "reject"}], - "transports": { - "docker": { - "registry.example.com/app": [{ - "type": "sigstoreSigned", - "keyDatas": ["%s", "%s"] - }] - } - } - } - """ - .formatted( - SigstoreTestSupport.keyData(kp1.getPublic()), - SigstoreTestSupport.keyData(kp2.getPublic()))); + String.format( + "{\n" + + " \"default\": [{\"type\": \"reject\"}],\n" + + " \"transports\": {\n" + + " \"docker\": {\n" + + " \"registry.example.com/app\": [{\n" + + " \"type\": \"sigstoreSigned\",\n" + + " \"keyDatas\": [\"%s\", \"%s\"]\n" + + " }]\n" + + " }\n" + + " }\n" + + "}\n", + SigstoreTestSupport.keyData(kp1.getPublic()), SigstoreTestSupport.keyData(kp2.getPublic()))); ContainersPolicy policy = ContainersPolicy.newPolicy(path); // Accepted when signed by kp1 @@ -455,19 +447,17 @@ void sigstoreSignedKeyPathsDeserializesCorrectly(@TempDir Path dir) throws IOExc // language=json Files.writeString( path, - """ - { - "default": [{"type": "reject"}], - "transports": { - "docker": { - "registry.example.com/app": [{ - "type": "sigstoreSigned", - "keyPaths": ["/etc/pki/a.pub", "/etc/pki/b.pub"] - }] - } - } - } - """); + "{\n" + + " \"default\": [{\"type\": \"reject\"}],\n" + + " \"transports\": {\n" + + " \"docker\": {\n" + + " \"registry.example.com/app\": [{\n" + + " \"type\": \"sigstoreSigned\",\n" + + " \"keyPaths\": [\"/etc/pki/a.pub\", \"/etc/pki/b.pub\"]\n" + + " }]\n" + + " }\n" + + " }\n" + + "}\n"); ContainersPolicy policy = ContainersPolicy.newPolicy(path); List reqs = policy.resolveRequirements(Transport.DOCKER, "registry.example.com/app"); assertEquals(1, reqs.size()); @@ -485,18 +475,16 @@ void signedByGpgIsDeniedBecauseNotImplemented(@TempDir Path dir) throws IOExcept // language=json Files.writeString( path, - """ - { - "default": [{"type": "reject"}], - "transports": { - "docker": { - "registry.example.com/signed": [ - {"type": "signedBy", "keyType": "GPGKeys", "keyPath": "/etc/pki/containers/my-key.gpg"} - ] - } - } - } - """); + "{\n" + + " \"default\": [{\"type\": \"reject\"}],\n" + + " \"transports\": {\n" + + " \"docker\": {\n" + + " \"registry.example.com/signed\": [\n" + + " {\"type\": \"signedBy\", \"keyType\": \"GPGKeys\", \"keyPath\": \"/etc/pki/containers/my-key.gpg\"}\n" + + " ]\n" + + " }\n" + + " }\n" + + "}\n"); ContainersPolicy policy = ContainersPolicy.newPolicy(path); assertThrows( OrasException.class, @@ -510,19 +498,18 @@ void mixedScopesResolveAndVerifyIndependently(@TempDir Path dir) throws IOExcept // language=json Files.writeString( path, - """ - { - "default": [{"type": "reject"}], - "transports": { - "docker": { - "registry.example.com/open": [{"type": "insecureAcceptAnything"}], - "registry.example.com/blocked": [{"type": "reject"}], - "registry.example.com/secure": [{"type": "sigstoreSigned", "keyData": "%s"}] - } - } - } - """ - .formatted(SigstoreTestSupport.keyData(kp.getPublic()))); + String.format( + "{\n" + + " \"default\": [{\"type\": \"reject\"}],\n" + + " \"transports\": {\n" + + " \"docker\": {\n" + + " \"registry.example.com/open\": [{\"type\": \"insecureAcceptAnything\"}],\n" + + " \"registry.example.com/blocked\": [{\"type\": \"reject\"}],\n" + + " \"registry.example.com/secure\": [{\"type\": \"sigstoreSigned\", \"keyData\": \"%s\"}]\n" + + " }\n" + + " }\n" + + "}\n", + SigstoreTestSupport.keyData(kp.getPublic()))); ContainersPolicy policy = ContainersPolicy.newPolicy(path); // Open scope: accepted without signatures. @@ -565,17 +552,16 @@ private static ContainersPolicy sigstorePolicy(Path dir, String scopeKey, String // language=json Files.writeString( path, - """ - { - "default": [{"type": "reject"}], - "transports": { - "docker": { - "%s": [{"type": "sigstoreSigned", "%s": "%s"}] - } - } - } - """ - .formatted(scopeKey, keyField, keyValue)); + String.format( + "{\n" + + " \"default\": [{\"type\": \"reject\"}],\n" + + " \"transports\": {\n" + + " \"docker\": {\n" + + " \"%s\": [{\"type\": \"sigstoreSigned\", \"%s\": \"%s\"}]\n" + + " }\n" + + " }\n" + + "}\n", + scopeKey, keyField, keyValue)); return ContainersPolicy.newPolicy(path); } diff --git a/src/test/java/land/oras/policy/SigstoreTestSupport.java b/src/test/java/land/oras/policy/SigstoreTestSupport.java index cbf613ac..b41416dd 100644 --- a/src/test/java/land/oras/policy/SigstoreTestSupport.java +++ b/src/test/java/land/oras/policy/SigstoreTestSupport.java @@ -68,12 +68,7 @@ static KeyPair generateKeyPair() { static String publicKeyPem(PublicKey key) { String b64 = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.UTF_8)).encodeToString(key.getEncoded()); - return """ - -----BEGIN PUBLIC KEY----- - %s - -----END PUBLIC KEY----- - """ - .formatted(b64); + return String.format("-----BEGIN PUBLIC KEY-----\n" + "%s\n" + "-----END PUBLIC KEY-----\n", b64); } /** @@ -116,32 +111,31 @@ private static byte[] sign(PrivateKey privateKey, byte[] content) { private static String inTotoPayload(String imageHex) { // language=json - return """ - { - "_type": "https://in-toto.io/Statement/v1", - "subject": [{"digest": {"sha256": "%s"}, "annotations": {}}], - "predicateType": "https://sigstore.dev/cosign/sign/v1", - "predicate": {} - }""" - .formatted(imageHex); + return String.format( + "{\n" + + " \"_type\": \"https://in-toto.io/Statement/v1\",\n" + + " \"subject\": [{\"digest\": {\"sha256\": \"%s\"}, \"annotations\": {}}],\n" + + " \"predicateType\": \"https://sigstore.dev/cosign/sign/v1\",\n" + + " \"predicate\": {}\n" + + "}", + imageHex); } private static String bundleJson(byte[] payload, byte[] signature) { Base64.Encoder b64 = Base64.getEncoder(); // language=json - return """ - { - "mediaType": "%s", - "dsseEnvelope": { - "payload": "%s", - "payloadType": "%s", - "signatures": [{"sig": "%s"}] - } - }""" - .formatted( - Const.SIGSTORE_BUNDLE_MEDIA_TYPE, - b64.encodeToString(payload), - Const.IN_TOTO_PAYLOAD_TYPE, - b64.encodeToString(signature)); + return String.format( + "{\n" + + " \"mediaType\": \"%s\",\n" + + " \"dsseEnvelope\": {\n" + + " \"payload\": \"%s\",\n" + + " \"payloadType\": \"%s\",\n" + + " \"signatures\": [{\"sig\": \"%s\"}]\n" + + " }\n" + + "}", + Const.SIGSTORE_BUNDLE_MEDIA_TYPE, + b64.encodeToString(payload), + Const.IN_TOTO_PAYLOAD_TYPE, + b64.encodeToString(signature)); } } diff --git a/src/test/java/land/oras/policy/SigstoreVerifierTest.java b/src/test/java/land/oras/policy/SigstoreVerifierTest.java index 21cb200b..efbdb2ec 100644 --- a/src/test/java/land/oras/policy/SigstoreVerifierTest.java +++ b/src/test/java/land/oras/policy/SigstoreVerifierTest.java @@ -182,19 +182,18 @@ void policyVerificationPassesForSignedImageAndFailsWhenTampered(@TempDir Path di // language=json Files.writeString( policyPath, - """ - { - "default": [{"type": "reject"}], - "transports": { - "docker": { - "registry.example.com/app": [ - {"type": "sigstoreSigned", "keyPath": "%s"} - ] - } - } - } - """ - .formatted(keyFile.toString().replace("\\", "\\\\"))); + String.format( + "{\n" + + " \"default\": [{\"type\": \"reject\"}],\n" + + " \"transports\": {\n" + + " \"docker\": {\n" + + " \"registry.example.com/app\": [\n" + + " {\"type\": \"sigstoreSigned\", \"keyPath\": \"%s\"}\n" + + " ]\n" + + " }\n" + + " }\n" + + "}\n", + keyFile.toString().replace("\\", "\\\\"))); ContainersPolicy policy = ContainersPolicy.newPolicy(policyPath); @@ -226,18 +225,16 @@ void policyVerificationFailsWhenKeyIsMissing(@TempDir Path dir) throws Exception // language=json Files.writeString( policyPath, - """ - { - "default": [{"type": "reject"}], - "transports": { - "docker": { - "registry.example.com/app": [ - {"type": "sigstoreSigned", "keyPath": "/nonexistent/cosign.pub"} - ] - } - } - } - """); + "{\n" + + " \"default\": [{\"type\": \"reject\"}],\n" + + " \"transports\": {\n" + + " \"docker\": {\n" + + " \"registry.example.com/app\": [\n" + + " {\"type\": \"sigstoreSigned\", \"keyPath\": \"/nonexistent/cosign.pub\"}\n" + + " ]\n" + + " }\n" + + " }\n" + + "}\n"); ContainersPolicy policy = ContainersPolicy.newPolicy(policyPath); PolicyContext context = new PolicyContext( @@ -271,43 +268,37 @@ private static byte[] sign(PrivateKey priv, byte[] content, String algorithm) th private static String inTotoPayload(String imageHex) { // language=json - return """ - { - "_type": "https://in-toto.io/Statement/v1", - "subject": [{"digest": {"sha256": "%s"}, "annotations": {}}], - "predicateType": "https://sigstore.dev/cosign/sign/v1", - "predicate": {} - }""" - .formatted(imageHex); + return String.format( + "{\n" + + " \"_type\": \"https://in-toto.io/Statement/v1\",\n" + + " \"subject\": [{\"digest\": {\"sha256\": \"%s\"}, \"annotations\": {}}],\n" + + " \"predicateType\": \"https://sigstore.dev/cosign/sign/v1\",\n" + + " \"predicate\": {}\n" + + "}", + imageHex); } private static String bundleJson(byte[] payload, byte[] signature) { Base64.Encoder b64 = Base64.getEncoder(); // language=json - return """ - { - "mediaType": "%s", - "dsseEnvelope": { - "payload": "%s", - "payloadType": "%s", - "signatures": [{"sig": "%s"}] - } - }""" - .formatted( - Const.SIGSTORE_BUNDLE_MEDIA_TYPE, - b64.encodeToString(payload), - Const.IN_TOTO_PAYLOAD_TYPE, - b64.encodeToString(signature)); + return String.format( + "{\n" + + " \"mediaType\": \"%s\",\n" + + " \"dsseEnvelope\": {\n" + + " \"payload\": \"%s\",\n" + + " \"payloadType\": \"%s\",\n" + + " \"signatures\": [{\"sig\": \"%s\"}]\n" + + " }\n" + + "}", + Const.SIGSTORE_BUNDLE_MEDIA_TYPE, + b64.encodeToString(payload), + Const.IN_TOTO_PAYLOAD_TYPE, + b64.encodeToString(signature)); } private static String toPem(PublicKey key) { String b64 = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.UTF_8)).encodeToString(key.getEncoded()); - return """ - -----BEGIN PUBLIC KEY----- - %s - -----END PUBLIC KEY----- - """ - .formatted(b64); + return String.format("-----BEGIN PUBLIC KEY-----\n" + "%s\n" + "-----END PUBLIC KEY-----\n", b64); } } diff --git a/src/test/java/land/oras/utils/TomlUtilsTest.java b/src/test/java/land/oras/utils/TomlUtilsTest.java index 7f13eff6..eb52102e 100644 --- a/src/test/java/land/oras/utils/TomlUtilsTest.java +++ b/src/test/java/land/oras/utils/TomlUtilsTest.java @@ -53,10 +53,7 @@ void failToParseTomlString() { @Test @SuppressWarnings("unchecked") void shouldParseToml() { - String tomlMap = """ - key1 = "value1" - key2 = "value2" - """; + String tomlMap = "key1 = \"value1\"\n" + "key2 = \"value2\"\n"; Map map = TomlUtils.fromToml(tomlMap, Map.class); assertNotNull(map); assertEquals(2, map.size()); @@ -67,10 +64,7 @@ void shouldParseToml() { @Test @SuppressWarnings("unchecked") void shouldParseTomlFile() throws IOException { - String tomlMap = """ - key1 = "value1" - key2 = "value2" - """; + String tomlMap = "key1 = \"value1\"\n" + "key2 = \"value2\"\n"; Path tomlFile = dir.resolve("test.toml"); Files.writeString(tomlFile, tomlMap); Map map = TomlUtils.fromToml(tomlFile, Map.class); @@ -87,10 +81,7 @@ void shouldConvertToToml() { map.put("key2", "value2"); String toml = TomlUtils.toToml(map); assertNotNull(toml); - String expected = """ - key1 = 'value1' - key2 = 'value2' - """; + String expected = "key1 = 'value1'\n" + "key2 = 'value2'\n"; assertEquals(expected, toml); } } diff --git a/src/test/java/land/oras/utils/YamlUtilsTest.java b/src/test/java/land/oras/utils/YamlUtilsTest.java index 571eb6bd..5eb663b2 100644 --- a/src/test/java/land/oras/utils/YamlUtilsTest.java +++ b/src/test/java/land/oras/utils/YamlUtilsTest.java @@ -53,12 +53,7 @@ void failToParseYamlString() { @Test @SuppressWarnings("unchecked") void shouldParseYaml() { - String yamlMap = - """ - --- - key1: "value1" - key2: "value2" - """; + String yamlMap = "---\n" + "key1: \"value1\"\n" + "key2: \"value2\"\n"; Map map = YamlUtils.fromYaml(yamlMap, Map.class); assertNotNull(map); assertEquals(2, map.size()); @@ -69,12 +64,7 @@ void shouldParseYaml() { @Test @SuppressWarnings("unchecked") void shouldParseYamlFile() throws IOException { - String yamlMap = - """ - --- - key1: "value1" - key2: "value2" - """; + String yamlMap = "---\n" + "key1: \"value1\"\n" + "key2: \"value2\"\n"; Path yamlFile = dir.resolve("test.yaml"); Files.writeString(yamlFile, yamlMap); Map map = YamlUtils.fromYaml(yamlFile, Map.class); @@ -97,11 +87,7 @@ void shouldConvertToYaml() { map.put("key2", "value2"); String yaml = YamlUtils.toYaml(map); assertNotNull(yaml); - String expected = """ - --- - key1: "value1" - key2: "value2" - """; + String expected = "---\n" + "key1: \"value1\"\n" + "key2: \"value2\"\n"; assertEquals(expected, yaml); } } diff --git a/src/test/java/land/oras/utils/ZotContainer.java b/src/test/java/land/oras/utils/ZotContainer.java index a63d1c1a..862e062a 100644 --- a/src/test/java/land/oras/utils/ZotContainer.java +++ b/src/test/java/land/oras/utils/ZotContainer.java @@ -44,23 +44,21 @@ public ZotContainer() { copyFileToContainer(authFile, "/etc/zot/auth.htpasswd"); // language=JSON - String configJson = - """ - { - "storage": { "rootDirectory": "/var/lib/registry" }, - "http": { - "address": "0.0.0.0", - "port": %s, - "auth": { - "htpasswd": { "path": "/etc/zot/auth.htpasswd" } - } - }, - "extensions": { - "search": { "enable": true } - } - } - """ - .formatted(ZOT_PORT); + String configJson = String.format( + "{\n" + + " \"storage\": { \"rootDirectory\": \"/var/lib/registry\" },\n" + + " \"http\": {\n" + + " \"address\": \"0.0.0.0\",\n" + + " \"port\": %s,\n" + + " \"auth\": {\n" + + " \"htpasswd\": { \"path\": \"/etc/zot/auth.htpasswd\" }\n" + + " }\n" + + " },\n" + + " \"extensions\": {\n" + + " \"search\": { \"enable\": true }\n" + + " }\n" + + "}\n", + ZOT_PORT); writeConfig(configJson); } catch (Exception e) { diff --git a/src/test/java/land/oras/utils/ZotTlsContainer.java b/src/test/java/land/oras/utils/ZotTlsContainer.java index 2c3328ae..9bd12193 100644 --- a/src/test/java/land/oras/utils/ZotTlsContainer.java +++ b/src/test/java/land/oras/utils/ZotTlsContainer.java @@ -66,24 +66,22 @@ public ZotTlsContainer() { copyFileToContainer(serverKeyPath, "/etc/zot/server-key.pem"); // language=JSON - String configJson = - """ - { - "storage": { "rootDirectory": "/var/lib/registry" }, - "http": { - "address": "0.0.0.0", - "port": %s, - "tls": { - "cert": "/etc/zot/server.pem", - "key": "/etc/zot/server-key.pem" - } - }, - "extensions": { - "search": { "enable": true } - } - } - """ - .formatted(ZOT_PORT); + String configJson = String.format( + "{\n" + + " \"storage\": { \"rootDirectory\": \"/var/lib/registry\" },\n" + + " \"http\": {\n" + + " \"address\": \"0.0.0.0\",\n" + + " \"port\": %s,\n" + + " \"tls\": {\n" + + " \"cert\": \"/etc/zot/server.pem\",\n" + + " \"key\": \"/etc/zot/server-key.pem\"\n" + + " }\n" + + " },\n" + + " \"extensions\": {\n" + + " \"search\": { \"enable\": true }\n" + + " }\n" + + "}\n", + ZOT_PORT); writeConfig(configJson); } catch (Exception e) { diff --git a/src/test/java/land/oras/utils/ZotUnsecureContainer.java b/src/test/java/land/oras/utils/ZotUnsecureContainer.java index ea7a136a..f5d98d60 100644 --- a/src/test/java/land/oras/utils/ZotUnsecureContainer.java +++ b/src/test/java/land/oras/utils/ZotUnsecureContainer.java @@ -33,20 +33,18 @@ public ZotUnsecureContainer() { setWaitStrategy(Wait.forHttp("/v2/_catalog").forPort(ZOT_PORT).forStatusCode(200)); // language=JSON - String configJson = - """ - { - "storage": { "rootDirectory": "/var/lib/registry" }, - "http": { - "address": "0.0.0.0", - "port": %s - }, - "extensions": { - "search": { "enable": true } - } - } - """ - .formatted(ZOT_PORT); + String configJson = String.format( + "{\n" + + " \"storage\": { \"rootDirectory\": \"/var/lib/registry\" },\n" + + " \"http\": {\n" + + " \"address\": \"0.0.0.0\",\n" + + " \"port\": %s\n" + + " },\n" + + " \"extensions\": {\n" + + " \"search\": { \"enable\": true }\n" + + " }\n" + + "}\n", + ZOT_PORT); writeConfig(configJson); } } diff --git a/updatecli/updatecli.d/maven.yaml b/updatecli/updatecli.d/maven.yaml deleted file mode 100644 index 86cd11bd..00000000 --- a/updatecli/updatecli.d/maven.yaml +++ /dev/null @@ -1,66 +0,0 @@ ---- -name: Bump the Maven - -scms: - default: - kind: github - spec: - user: "{{ .github.user }}" - email: "{{ .github.email }}" - owner: "{{ .github.owner }}" - repository: "{{ .github.repository }}" - token: "{{ requiredEnv .github.token }}" - username: "{{ .github.username }}" - branch: "{{ .github.branch }}" - -sources: - mavenLatestVersion: - kind: githubrelease - spec: - owner: "apache" - repository: "maven" - token: "{{ requiredEnv .github.token }}" - username: "{{ .github.username }}" - versionFilter: - kind: regex - pattern: "maven-3.(\\d{1,2}).(\\d{1,2})$" - transformers: - - trimprefix: "maven-" - -targets: - updatePom: - name: "Update the value in the pom.xml file" - kind: file - sourceid: mavenLatestVersion - spec: - file: pom.xml - matchPattern: "\\[(\\d.\\d.\\d{1,2}),\\)" - replacePattern: '[{{ source "mavenLatestVersion" }},)' - scmid: default - updateSdkmanRC: - name: "Update the value in the .sdkmanrc file" - kind: file - sourceid: mavenLatestVersion - spec: - file: .sdkmanrc - matchPattern: "maven=(\\d.\\d.\\d{1,2})" - replacePattern: 'maven={{ source "mavenLatestVersion" }}' - scmid: default - updateWorkflowFiles: - name: "Update the value in the .sdkmanrc file" - kind: file - sourceid: mavenLatestVersion - spec: - file: .github/vars/maven-version.txt - matchPattern: "(\\d.\\d.\\d{1,2})" - replacePattern: '{{ source "mavenLatestVersion" }}' - scmid: default - -actions: - default: - kind: github/pullrequest - scmid: default - title: 'Bump Maven {{ source "mavenLatestVersion" }}' - spec: - labels: - - dependencies diff --git a/updatecli/updatecli.d/zot.yaml b/updatecli/updatecli.d/zot.yaml deleted file mode 100644 index f659e55a..00000000 --- a/updatecli/updatecli.d/zot.yaml +++ /dev/null @@ -1,53 +0,0 @@ ---- -name: Bump zot test container - -scms: - default: - kind: github - spec: - user: "{{ .github.user }}" - email: "{{ .github.email }}" - owner: "{{ .github.owner }}" - repository: "{{ .github.repository }}" - token: "{{ requiredEnv .github.token }}" - username: "{{ .github.username }}" - branch: "{{ .github.branch }}" - -sources: - zotLatestVersion: - kind: githubrelease - spec: - owner: project-zot - repository: zot - token: "{{ requiredEnv .github.token }}" - username: "{{ .github.username }}" - versioning: - kind: semver - -conditions: - zotImageExists: - name: zot image must exists - kind: dockerimage - spec: - image: ghcr.io/project-zot/zot-minimal - -targets: - updateZotImages: - name: "Update the value in the TestImages.java file" - kind: file - sourceid: zotLatestVersion - spec: - file: src/test/java/land/oras/utils/TestImages.java - matchPattern: "ghcr.io/project-zot/zot-minimal:v(\\d.\\d.\\d{1,2})" - replacePattern: 'ghcr.io/project-zot/zot-minimal:{{ source "zotLatestVersion" }}' - scmid: default - -actions: - default: - kind: github/pullrequest - scmid: default - title: 'Bump zot {{ source "zotLatestVersion" }}' - spec: - labels: - - tests - - dependencies diff --git a/updatecli/values.github-action.yaml b/updatecli/values.github-action.yaml deleted file mode 100644 index 89dd0020..00000000 --- a/updatecli/values.github-action.yaml +++ /dev/null @@ -1,8 +0,0 @@ -github: - user: "github-actions[bot]" - email: "41898282+github-actions[bot]@users.noreply.github.com" - username: "github-actions" - token: "UPDATECLI_GITHUB_TOKEN" - branch: "main" - owner: "oras-project" - repository: "oras-java"