Compare commits

..
Author SHA1 Message Date
mickael emirkanian 1c3c00dd7f ci: update workflows to test action
test the tracebit action

Signed-off-by: mickael emirkanian <mickael.emirkanian@docker.com>
2026-09-24 14:19:55 -04:00
184 changed files with 4920 additions and 11470 deletions
-59
View File
@@ -1,59 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
// https://docs.renovatebot.com/modules/manager/github-actions/
// https://docs.renovatebot.com/presets-helpers/
"helpers:pinGitHubActionDigestsToSemver",
":gitSignOff"
],
"gomod": {
"managerFilePatterns": ["/(^|/)vendor\\.mod$/"]
},
"postUpdateOptions": ["gomodTidy"],
"minimumReleaseAge": "3 days",
"internalChecksFilter": "strict",
"packageRules": [
{
"matchManagers": ["gomod"],
"matchFileNames": ["vendor.mod"],
"semanticCommits": "disabled",
"commitMessagePrefix": "vendor:"
},
{
"matchManagers": ["gomod"],
"matchFileNames": ["cmd/docker-trust/go.mod"],
// The trust module does not vendor dependencies; vendor/ belongs to the root module.
"postUpdateOptions": ["gomodSkipVendor"],
"semanticCommits": "disabled",
"commitMessagePrefix": "cmd/docker-trust:"
},
{
"matchManagers": ["gomod"],
"matchFileNames": ["vendor.mod"],
"groupName": "golang.org/x packages",
"groupSlug": "golang-x",
"matchDepNames": ["/^golang\\.org\\/x\\//"]
},
{
"matchManagers": ["github-actions"],
"minimumReleaseAge": "7 days",
"addLabels": ["area/ci", "status/2-code-review"]
},
{
"matchManagers": ["github-actions"],
"groupName": "docker GitHub Actions",
"groupSlug": "docker-actions",
"matchDepNames": ["/^docker\\//"]
},
{
// Keep all the google.golang.org/genproto modules at the same version.
"matchManagers": ["gomod"],
"matchFileNames": ["vendor.mod"],
"groupName": "google.golang.org/genproto packages",
"groupSlug": "google-genproto",
"matchDepNames": ["/^google\\.golang\\.org\\/genproto(\\/|$)/"],
"schedule": ["every month"]
}
]
}
+35 -3
View File
@@ -33,6 +33,14 @@ jobs:
outputs: outputs:
matrix: ${{ steps.platforms.outputs.matrix }} matrix: ${{ steps.platforms.outputs.matrix }}
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- -
name: Checkout name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -63,9 +71,17 @@ jobs:
- "" - ""
- glibc - glibc
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- -
name: Build name: Build
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0 uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
@@ -91,7 +107,7 @@ jobs:
PLATFORM: ${{ matrix.platform }} PLATFORM: ${{ matrix.platform }}
- -
name: Upload artifacts name: Upload artifacts
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: ${{ env.ARTIFACT_NAME }} name: ${{ env.ARTIFACT_NAME }}
path: /tmp/out/* path: /tmp/out/*
@@ -130,6 +146,14 @@ jobs:
outputs: outputs:
matrix: ${{ steps.platforms.outputs.matrix }} matrix: ${{ steps.platforms.outputs.matrix }}
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- -
name: Checkout name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -154,9 +178,17 @@ jobs:
matrix: matrix:
platform: ${{ fromJson(needs.prepare-plugins.outputs.matrix) }} platform: ${{ fromJson(needs.prepare-plugins.outputs.matrix) }}
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- -
name: Build name: Build
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0 uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
+4 -4
View File
@@ -64,18 +64,18 @@ jobs:
name: Update Go name: Update Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with: with:
go-version: "1.26.10" go-version: "1.26.8"
cache: false cache: false
- -
name: Initialize CodeQL name: Initialize CodeQL
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
with: with:
languages: go languages: go
- -
name: Autobuild name: Autobuild
uses: github/codeql-action/autobuild@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 uses: github/codeql-action/autobuild@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
- -
name: Perform CodeQL Analysis name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
with: with:
category: "/language:go" category: "/language:go"
+9 -2
View File
@@ -37,12 +37,19 @@ jobs:
- alpine - alpine
- debian - debian
engine-version: engine-version:
- master # moby/moby-bin:master
- rc # latest rc - rc # latest rc
- 29 # latest - 29 # latest
- 28 # latest - 1 - 28 # latest - 1
- 25 # mirantis lts - 25 # mirantis lts
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- -
name: Checkout name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -66,7 +73,7 @@ jobs:
docker info docker info
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- -
name: Run ${{ matrix.target }} name: Run ${{ matrix.target }}
run: | run: |
+9 -1
View File
@@ -20,6 +20,14 @@ jobs:
if: github.event.pull_request.head.repo.fork if: github.event.pull_request.head.repo.fork
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- name: Save event context - name: Save event context
env: env:
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event.pull_request.number }}
@@ -35,7 +43,7 @@ jobs:
fi fi
- name: Upload context - name: Upload context
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: pr-review-context name: pr-review-context
path: context/ path: context/
+18 -2
View File
@@ -28,9 +28,17 @@ jobs:
ctn: ctn:
runs-on: ubuntu-26.04 runs-on: ubuntu-26.04
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- -
name: Test name: Test
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0 uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
@@ -58,6 +66,14 @@ jobs:
- macos-15 # macOS 15 on arm64 (Apple Silicon M1) - macos-15 # macOS 15 on arm64 (Apple Silicon M1)
# - windows-2022 # FIXME: some tests are failing on the Windows runner, as well as on Appveyor since June 24, 2018: https://ci.appveyor.com/project/docker/cli/history # - windows-2022 # FIXME: some tests are failing on the Windows runner, as well as on Appveyor since June 24, 2018: https://ci.appveyor.com/project/docker/cli/history
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- -
name: Checkout name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -68,7 +84,7 @@ jobs:
name: Set up Go name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with: with:
go-version: "1.26.10" go-version: "1.26.8"
cache: false cache: false
- -
name: Test name: Test
+8
View File
@@ -13,6 +13,14 @@ jobs:
runs-on: ubuntu-26.04 runs-on: ubuntu-26.04
timeout-minutes: 5 timeout-minutes: 5
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- name: Validate milestone matches VERSION - name: Validate milestone matches VERSION
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env: env:
+24
View File
@@ -18,6 +18,14 @@ jobs:
runs-on: ubuntu-26.04 runs-on: ubuntu-26.04
timeout-minutes: 120 # guardrails timeout for the whole job timeout-minutes: 120 # guardrails timeout for the whole job
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- name: Missing `area/` label - name: Missing `area/` label
if: always() && contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/') if: always() && contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/')
run: | run: |
@@ -39,6 +47,14 @@ jobs:
PR_BODY: | PR_BODY: |
${{ github.event.pull_request.body }} ${{ github.event.pull_request.body }}
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- name: Check changelog description - name: Check changelog description
run: | run: |
# Extract the `markdown changelog` note code block # Extract the `markdown changelog` note code block
@@ -75,6 +91,14 @@ jobs:
env: env:
PR_TITLE: ${{ github.event.pull_request.title }} PR_TITLE: ${{ github.event.pull_request.title }}
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
# Backports or PR that target a release branch directly should mention the target branch in the title, for example: # Backports or PR that target a release branch directly should mention the target branch in the title, for example:
# [X.Y backport] Some change that needs backporting to X.Y # [X.Y backport] Some change that needs backporting to X.Y
# [X.Y] Change directly targeting the X.Y branch # [X.Y] Change directly targeting the X.Y branch
+33 -1
View File
@@ -36,6 +36,14 @@ jobs:
- validate-vendor - validate-vendor
- update-authors # ensure authors update target runs fine - update-authors # ensure authors update target runs fine
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- -
name: Run name: Run
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0 uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
@@ -46,6 +54,14 @@ jobs:
validate-md: validate-md:
runs-on: ubuntu-26.04 runs-on: ubuntu-26.04
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- -
name: Checkout name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -74,6 +90,14 @@ jobs:
- yamldocs # ensure yamldocs target runs fine - yamldocs # ensure yamldocs target runs fine
- manpages # ensure manpages target runs fine - manpages # ensure manpages target runs fine
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- -
name: Checkout name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -91,6 +115,14 @@ jobs:
GOPATH: ${{ github.workspace }} GOPATH: ${{ github.workspace }}
GO111MODULE: off GO111MODULE: off
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- -
name: Checkout name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -101,7 +133,7 @@ jobs:
name: Set up Go name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with: with:
go-version: "1.26.10" go-version: "1.26.8"
cache: false cache: false
- -
name: Run gocompat check name: Run gocompat check
+1 -1
View File
@@ -5,7 +5,7 @@ run:
# which causes it to fallback to go1.17 semantics. # which causes it to fallback to go1.17 semantics.
# #
# TODO(thaJeztah): update "usetesting" settings to enable go1.24 features once our minimum version is go1.24 # TODO(thaJeztah): update "usetesting" settings to enable go1.24 features once our minimum version is go1.24
go: "1.26.10" go: "1.26.8"
timeout: 5m timeout: 5m
+4 -4
View File
@@ -8,11 +8,11 @@ ARG BASE_VARIANT=alpine
ARG ALPINE_VERSION=3.23 ARG ALPINE_VERSION=3.23
ARG BASE_DEBIAN_DISTRO=bookworm ARG BASE_DEBIAN_DISTRO=bookworm
ARG GO_VERSION=1.26.10 ARG GO_VERSION=1.26.8
# XX_VERSION specifies the version of the xx utility to use. # XX_VERSION specifies the version of the xx utility to use.
# It must be a valid tag in the docker.io/tonistiigi/xx image repository. # It must be a valid tag in the docker.io/tonistiigi/xx image repository.
ARG XX_VERSION=1.10.0 ARG XX_VERSION=1.9.0
# GOVERSIONINFO_VERSION is the version of GoVersionInfo to install. # GOVERSIONINFO_VERSION is the version of GoVersionInfo to install.
# It must be a valid tag from https://github.com/josephspurrier/goversioninfo # It must be a valid tag from https://github.com/josephspurrier/goversioninfo
@@ -25,12 +25,12 @@ ARG GOTESTSUM_VERSION=v1.13.0
# BUILDX_VERSION sets the version of buildx to use for the e2e tests. # BUILDX_VERSION sets the version of buildx to use for the e2e tests.
# It must be a tag in the docker.io/docker/buildx-bin image repository # It must be a tag in the docker.io/docker/buildx-bin image repository
# on Docker Hub. # on Docker Hub.
ARG BUILDX_VERSION=0.38.0 ARG BUILDX_VERSION=0.34.1
# COMPOSE_VERSION is the version of compose to install in the dev container. # COMPOSE_VERSION is the version of compose to install in the dev container.
# It must be a tag in the docker.io/docker/compose-bin image repository # It must be a tag in the docker.io/docker/compose-bin image repository
# on Docker Hub. # on Docker Hub.
ARG COMPOSE_VERSION=v5.6.0 ARG COMPOSE_VERSION=v5.1.3
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
+1
View File
@@ -5,6 +5,7 @@
[![Test Status](https://img.shields.io/github/actions/workflow/status/docker/cli/test.yml?branch=master&label=test&logo=github)](https://github.com/docker/cli/actions?query=workflow%3Atest) [![Test Status](https://img.shields.io/github/actions/workflow/status/docker/cli/test.yml?branch=master&label=test&logo=github)](https://github.com/docker/cli/actions?query=workflow%3Atest)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/docker/cli/badge)](https://scorecard.dev/viewer/?uri=github.com/docker/cli) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/docker/cli/badge)](https://scorecard.dev/viewer/?uri=github.com/docker/cli)
[![Codecov](https://img.shields.io/codecov/c/github/docker/cli?logo=codecov)](https://codecov.io/gh/docker/cli) [![Codecov](https://img.shields.io/codecov/c/github/docker/cli?logo=codecov)](https://codecov.io/gh/docker/cli)
[![Protected by Tracebit Community Edition](https://github.com/tracebit-com/tracebit-community-action/blob/main/assets/badges/tracebit-badge-compact.svg?raw=1)](https://community.tracebit.com/supplychainattacks)
## About ## About
+1 -1
View File
@@ -1 +1 @@
29.9.3 29.8.2
-4
View File
@@ -30,11 +30,7 @@ package hooks
type ResponseType int type ResponseType int
const ( const (
// NextSteps renders suggestions under a shared "What's next:" header.
NextSteps ResponseType = 0 NextSteps ResponseType = 0
// GenericMessage renders before NextSteps, without a header or indentation.
// CLI versions that do not recognize this type ignore the response.
GenericMessage ResponseType = 1
) )
// Request is the type representing the information // Request is the type representing the information
+10 -35
View File
@@ -7,42 +7,17 @@ const (
indent = " " indent = " "
) )
// PrintMessages groups messages by type and writes them to out. Generic messages
// appear first, without a header or indentation, followed by next steps under a
// shared "What's next:" header. Order within each type is preserved.
// Messages with no lines or an unknown type produce no output.
func PrintMessages(out io.Writer, messages []Response, evaluate func(Response) []string) {
grouped := make(map[ResponseType][]string)
for _, message := range messages {
lines := evaluate(message)
grouped[message.Type] = append(grouped[message.Type], lines...)
}
for _, responseType := range []ResponseType{GenericMessage, NextSteps} {
lines := grouped[responseType]
if len(lines) == 0 {
continue
}
header, prefix := "\n", ""
if responseType == NextSteps {
header, prefix = whatsNext, indent
}
_, _ = io.WriteString(out, header)
for _, line := range lines {
_, _ = io.WriteString(out, prefix)
_, _ = io.WriteString(out, line)
_, _ = io.WriteString(out, "\n")
}
}
}
// PrintNextSteps renders list of [NextSteps] messages and writes them // PrintNextSteps renders list of [NextSteps] messages and writes them
// to out. It is a no-op if messages is empty. // to out. It is a no-op if messages is empty.
//
// Deprecated: use [PrintMessages] instead.
func PrintNextSteps(out io.Writer, messages []string) { func PrintNextSteps(out io.Writer, messages []string) {
PrintMessages(out, []Response{{Type: NextSteps}}, func(Response) []string { if len(messages) == 0 {
return messages return
}) }
_, _ = io.WriteString(out, whatsNext)
for _, msg := range messages {
_, _ = io.WriteString(out, indent)
_, _ = io.WriteString(out, msg)
_, _ = io.WriteString(out, "\n")
}
} }
+1 -84
View File
@@ -8,85 +8,7 @@ import (
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
) )
func TestPrintMessages(t *testing.T) { func TestPrintHookMessages(t *testing.T) {
const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
for _, tc := range []struct {
doc string
messages []hooks.Response
expected string
}{
{doc: "no messages"},
{
doc: "no lines",
messages: []hooks.Response{
{Type: hooks.GenericMessage},
{Type: hooks.NextSteps},
},
},
{
doc: "generic message",
messages: []hooks.Response{
{Type: hooks.GenericMessage, Template: "Session active"},
},
expected: "\nSession active\n",
},
{
doc: "preserve message formatting",
messages: []hooks.Response{
{Type: hooks.GenericMessage, Template: "Session active\n\n Details"},
},
expected: "\nSession active\n\n Details\n",
},
{
doc: "next steps",
messages: []hooks.Response{
{Type: hooks.NextSteps, Template: "Try another command"},
},
expected: header + " Try another command\n",
},
{
doc: "group interleaved types and preserve order within each type",
messages: []hooks.Response{
{Type: hooks.NextSteps, Template: "First suggestion"},
{Type: hooks.GenericMessage, Template: "Session active"},
{Type: hooks.NextSteps, Template: "Second suggestion\nThird suggestion"},
{Type: hooks.GenericMessage, Template: "Another status"},
},
expected: "\nSession active\nAnother status\n" + header + " First suggestion\n Second suggestion\n Third suggestion\n",
},
{
doc: "unknown type",
messages: []hooks.Response{
{Type: hooks.ResponseType(99), Template: "Ignored"},
},
},
{
doc: "unknown type does not affect known messages",
messages: []hooks.Response{
{Type: hooks.ResponseType(99), Template: "Ignored"},
{Type: hooks.GenericMessage, Template: "Session active"},
{Type: hooks.NextSteps, Template: "Try another command"},
},
expected: "\nSession active\n" + header + " Try another command\n",
},
} {
t.Run(tc.doc, func(t *testing.T) {
var w strings.Builder
hooks.PrintMessages(&w, tc.messages, func(message hooks.Response) []string {
if message.Template == "" {
return nil
}
lines, err := hooks.ParseTemplate(message.Template, nil)
assert.NilError(t, err)
return lines
})
assert.Equal(t, w.String(), tc.expected)
})
}
}
func TestPrintNextSteps(t *testing.T) {
const header = "\n\x1b[1mWhat's next:\x1b[0m\n" const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
tests := []struct { tests := []struct {
@@ -112,11 +34,6 @@ func TestPrintNextSteps(t *testing.T) {
" Foo\n" + " Foo\n" +
" bar\n", " bar\n",
}, },
{
doc: "preserve empty and indented lines",
messages: []string{"", " Details", ""},
expectedOutput: header + " \n Details\n \n",
},
} }
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) { t.Run(tc.doc, func(t *testing.T) {
+1 -11
View File
@@ -7,7 +7,6 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"slices"
"strings" "strings"
"text/template" "text/template"
@@ -44,16 +43,7 @@ func ParseTemplate(hookTemplate string, cmd *cobra.Command) ([]string, error) {
if n := strings.Count(out, "\n"); n > maxMessages { if n := strings.Count(out, "\n"); n > maxMessages {
return nil, fmt.Errorf("hook template contains too many messages (%d): maximum is %d", n, maxMessages) return nil, fmt.Errorf("hook template contains too many messages (%d): maximum is %d", n, maxMessages)
} }
lines := strings.SplitN(out, "\n", maxMessages) return strings.SplitN(out, "\n", maxMessages), nil
// Skip messages containing only whitespace, but preserve empty lines
// in messages that have content (e.g., for paragraph spacing).
if !slices.ContainsFunc(lines, func(line string) bool {
return strings.TrimSpace(line) != ""
}) {
return nil, nil
}
return lines, nil
} }
var ErrHookTemplateParse = errors.New("failed to parse hook template") var ErrHookTemplateParse = errors.New("failed to parse hook template")
+1 -11
View File
@@ -33,17 +33,7 @@ func TestParseTemplate(t *testing.T) {
{ {
doc: "empty template", doc: "empty template",
template: "", template: "",
expectedOutput: nil, expectedOutput: []string{""},
},
{
doc: "only empty lines",
template: "\n\n",
expectedOutput: nil,
},
{
doc: "whitespace-only lines",
template: " \n\t\n ",
expectedOutput: nil,
}, },
{ {
doc: "plain message", doc: "plain message",
+47 -23
View File
@@ -49,18 +49,11 @@ func RunPluginHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, sub
} }
func runHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) { func runHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
messages := invokeAndCollectHooks(ctx, cfg, rootCmd, invokedCommand, flags, cmdErrorMessage) nextSteps := invokeAndCollectHooks(ctx, cfg, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
hooks.PrintMessages(subCommand.ErrOrStderr(), messages, func(message hooks.Response) []string { hooks.PrintNextSteps(subCommand.ErrOrStderr(), nextSteps)
lines, err := hooks.ParseTemplate(message.Template, subCommand)
if err != nil {
logrus.WithError(err).Debug("Failed to evaluate hook template")
return nil
}
return lines
})
} }
func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []hooks.Response { func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
if ctx.Err() != nil { if ctx.Err() != nil {
return nil return nil
} }
@@ -71,17 +64,17 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
} }
pluginDirs := getPluginDirs(cfg) pluginDirs := getPluginDirs(cfg)
var collected []hooks.Response nextSteps := make([]string, 0, len(pluginsCfg))
tryInvokeHook := func(pluginName string, pluginCfg map[string]string) (hooks.Response, error) { tryInvokeHook := func(pluginName string, pluginCfg map[string]string) (messages []string, ok bool, err error) {
match, matched := pluginMatch(pluginCfg, subCmdStr, cmdErrorMessage) match, matched := pluginMatch(pluginCfg, subCmdStr, cmdErrorMessage)
if !matched { if !matched {
return hooks.Response{}, nil return nil, false, nil
} }
p, err := getPlugin(pluginName, pluginDirs, rootCmd) p, err := getPlugin(pluginName, pluginDirs, rootCmd)
if err != nil { if err != nil {
return hooks.Response{}, err return nil, false, err
} }
resp, err := p.RunHook(ctx, hooks.Request{ resp, err := p.RunHook(ctx, hooks.Request{
@@ -90,23 +83,29 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
CommandError: cmdErrorMessage, CommandError: cmdErrorMessage,
}) })
if err != nil { if err != nil {
return hooks.Response{}, err return nil, false, err
} }
var message hooks.Response var message hooks.Response
if err := json.Unmarshal(resp, &message); err != nil { if err := json.Unmarshal(resp, &message); err != nil {
return hooks.Response{}, fmt.Errorf("failed to unmarshal hook response (%q): %w", string(resp), err) return nil, false, fmt.Errorf("failed to unmarshal hook response (%q): %w", string(resp), err)
} }
if message.Type != hooks.NextSteps && message.Type != hooks.GenericMessage { // currently the only hook type
return hooks.Response{}, errors.New("unexpected hook response type: " + strconv.Itoa(int(message.Type))) if message.Type != hooks.NextSteps {
return nil, false, errors.New("unexpected hook response type: " + strconv.Itoa(int(message.Type)))
} }
return message, nil messages, err = hooks.ParseTemplate(message.Template, subCmd)
if err != nil {
return nil, false, err
}
return messages, true, nil
} }
for pluginName, pluginCfg := range pluginsCfg { for pluginName, pluginCfg := range pluginsCfg {
message, err := tryInvokeHook(pluginName, pluginCfg) messages, ok, err := tryInvokeHook(pluginName, pluginCfg)
if err != nil { if err != nil {
// skip misbehaving plugins, but don't halt execution // skip misbehaving plugins, but don't halt execution
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
@@ -115,13 +114,38 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
}).Debug("Plugin hook invocation failed") }).Debug("Plugin hook invocation failed")
continue continue
} }
if message.Template == "" { if !ok {
continue continue
} }
collected = append(collected, message) var appended bool
nextSteps, appended = appendNextSteps(nextSteps, messages)
if !appended {
logrus.WithFields(logrus.Fields{
"plugin": pluginName,
}).Debug("Plugin responded with an empty hook message; ignoring")
}
} }
return collected return nextSteps
}
// appendNextSteps appends the processed hook output to the nextSteps slice.
// If the processed hook output is empty, it is not appended.
// Empty lines are not stripped if there's at least one non-empty line.
func appendNextSteps(nextSteps []string, processed []string) ([]string, bool) {
empty := true
for _, l := range processed {
if strings.TrimSpace(l) != "" {
empty = false
break
}
}
if empty {
return nextSteps, false
}
return append(nextSteps, processed...), true
} }
// pluginMatch takes a plugin configuration and a string representing the // pluginMatch takes a plugin configuration and a string representing the
+35 -108
View File
@@ -2,11 +2,6 @@ package manager
import ( import (
"context" "context"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"testing" "testing"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
@@ -263,6 +258,38 @@ func TestMatchHookConfig(t *testing.T) {
} }
} }
func TestAppendNextSteps(t *testing.T) {
testCases := []struct {
processed []string
expectedOut []string
}{
{
processed: []string{},
expectedOut: []string{},
},
{
processed: []string{"", ""},
expectedOut: []string{},
},
{
processed: []string{"Some hint", "", "Some other hint"},
expectedOut: []string{"Some hint", "", "Some other hint"},
},
{
processed: []string{"Hint 1", "Hint 2"},
expectedOut: []string{"Hint 1", "Hint 2"},
},
}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
got, appended := appendNextSteps([]string{}, tc.processed)
assert.Check(t, is.DeepEqual(got, tc.expectedOut))
assert.Check(t, is.Equal(appended, len(got) > 0))
})
}
}
func TestRunPluginHooksPassesErrorMessage(t *testing.T) { func TestRunPluginHooksPassesErrorMessage(t *testing.T) {
cfg := configfile.New("") cfg := configfile.New("")
cfg.Plugins = map[string]map[string]string{ cfg.Plugins = map[string]map[string]string{
@@ -309,7 +336,7 @@ func TestInvokeAndCollectHooksErrorHooksSkippedOnSuccess(t *testing.T) {
// On success, error-hooks should not match, so the plugin // On success, error-hooks should not match, so the plugin
// binary is never looked up and no results are returned. // binary is never looked up and no results are returned.
result := invokeAndCollectHooks( result := invokeAndCollectHooks(
context.Background(), cfg, root, context.Background(), cfg, root, sub,
"build", map[string]string{}, "", "build", map[string]string{}, "",
) )
assert.Check(t, is.Len(result, 0)) assert.Check(t, is.Len(result, 0))
@@ -322,7 +349,7 @@ func TestInvokeAndCollectHooksNoPlugins(t *testing.T) {
root.AddCommand(sub) root.AddCommand(sub)
result := invokeAndCollectHooks( result := invokeAndCollectHooks(
context.Background(), cfg, root, context.Background(), cfg, root, sub,
"build", map[string]string{}, "some error", "build", map[string]string{}, "some error",
) )
assert.Check(t, is.Len(result, 0)) assert.Check(t, is.Len(result, 0))
@@ -341,108 +368,8 @@ func TestInvokeAndCollectHooksCancelledContext(t *testing.T) {
cancel() // cancel immediately cancel() // cancel immediately
result := invokeAndCollectHooks( result := invokeAndCollectHooks(
ctx, cfg, root, ctx, cfg, root, sub,
"build", map[string]string{}, "exit status 1", "build", map[string]string{}, "exit status 1",
) )
assert.Check(t, is.Nil(result)) assert.Check(t, is.Nil(result))
} }
func TestRunHooksMessages(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("test plugins require a POSIX shell")
}
const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
for _, tc := range []struct {
doc string
responses map[string]string
expected string
}{
{
doc: "generic message preserves formatting and expands templates",
responses: map[string]string{
"status": `{"Type":1,"Template":"Status for {{command}}\n\n Details"}`,
},
expected: "\nStatus for build\n\n Details\n",
},
{
doc: "omitted type defaults to next steps",
responses: map[string]string{
"suggestion": `{"Template":"Try another command"}`,
},
expected: header + " Try another command\n",
},
{
doc: "generic messages precede shared next steps header",
responses: map[string]string{
"statusone": `{"Type":1,"Template":"Session active"}`,
"statustwo": `{"Type":1,"Template":"Session active"}`,
"suggestionone": `{"Type":0,"Template":"Try another command"}`,
"suggestiontwo": `{"Type":0,"Template":"Try another command"}`,
},
expected: "\nSession active\nSession active\n" + header + " Try another command\n Try another command\n",
},
{
doc: "empty responses produce no output",
responses: map[string]string{
"status": `{"Type":1,"Template":" \n\t"}`,
"suggestion": `{"Type":0,"Template":""}`,
},
},
{
doc: "empty generic message does not affect next steps",
responses: map[string]string{
"status": `{"Type":1,"Template":""}`,
"suggestion": `{"Type":0,"Template":"Try another command"}`,
},
expected: header + " Try another command\n",
},
{
doc: "invalid responses do not affect other plugins",
responses: map[string]string{
"unknown": `{"Type":99,"Template":"Ignored"}`,
"invalid": `{"Type":1,"Template":"{{"}`,
"status": `{"Type":1,"Template":"Session active"}`,
"suggestion": `{"Type":0,"Template":"Try another command"}`,
},
expected: "\nSession active\n" + header + " Try another command\n",
},
} {
t.Run(tc.doc, func(t *testing.T) {
pluginDir := t.TempDir()
cfg := configfile.New("")
cfg.CLIPluginsExtraDirs = []string{pluginDir}
cfg.Plugins = make(map[string]map[string]string)
for name, response := range tc.responses {
plugin := fmt.Sprintf(`#!/bin/sh
case "$1" in
docker-cli-plugin-metadata)
printf '%%s\n' '{"SchemaVersion":"0.1.0","Vendor":"test"}'
;;
*)
test "$2" = docker-cli-plugin-hooks || exit 1
printf 'called\n' >> "$0.calls"
printf '%%s\n' '%s'
;;
esac
`, response)
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-"+name), []byte(plugin), 0o755))
cfg.Plugins[name] = map[string]string{"hooks": "build"}
}
root := &cobra.Command{Use: "docker"}
sub := &cobra.Command{Use: "build"}
root.AddCommand(sub)
var output strings.Builder
sub.SetErr(&output)
runHooks(context.Background(), cfg, root, sub, "build", nil, "")
assert.Equal(t, output.String(), tc.expected)
for name := range tc.responses {
calls, err := os.ReadFile(filepath.Join(pluginDir, "docker-"+name+".calls"))
assert.NilError(t, err)
assert.Equal(t, string(calls), "called\n", "each plugin hook must run exactly once")
}
})
}
}
-6
View File
@@ -1,6 +1,3 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package metadata package metadata
const ( const (
@@ -38,7 +35,4 @@ type Metadata struct {
URL string `json:",omitempty"` URL string `json:",omitempty"`
// Hidden hides the plugin in completion and help message output. // Hidden hides the plugin in completion and help message output.
Hidden bool `json:",omitempty"` Hidden bool `json:",omitempty"`
// Features declares optional contracts supported by the plugin, keyed by
// feature name.
Features map[string]any `json:",omitempty"`
} }
-13
View File
@@ -68,7 +68,6 @@ type DockerCli struct {
serverInfo ServerInfo serverInfo ServerInfo
contextStore store.Store contextStore store.Store
currentContext string currentContext string
contextResolver func(*DockerCli) (string, error)
init sync.Once init sync.Once
initErr error initErr error
dockerEndpoint docker.Endpoint dockerEndpoint docker.Endpoint
@@ -209,8 +208,6 @@ func (cli *DockerCli) HooksEnabled() bool {
// Initialize the dockerCli runs initialization that must happen after command // Initialize the dockerCli runs initialization that must happen after command
// line flags are parsed. // line flags are parsed.
//
//nolint:gocyclo
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) error { func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) error {
for _, o := range ops { for _, o := range ops {
if err := o(cli); err != nil { if err := o(cli); err != nil {
@@ -260,16 +257,6 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
}, },
} }
if cli.contextResolver != nil {
contextName, err := cli.contextResolver(cli)
if err != nil {
return err
}
if contextName != "" {
cli.currentContext = contextName
}
}
// TODO(krissetto): pass ctx to the funcs instead of using this // TODO(krissetto): pass ctx to the funcs instead of using this
if cli.enableGlobalMeter { if cli.enableGlobalMeter {
cli.createGlobalMeterProvider(cli.baseCtx) cli.createGlobalMeterProvider(cli.baseCtx)
-13
View File
@@ -84,19 +84,6 @@ func WithDefaultContextStoreConfig() CLIOption {
} }
} }
// WithContextResolver overrides context selection during [DockerCli.Initialize].
// The resolver runs after configuration and the context store are loaded, but
// before telemetry and endpoint initialization.
// An empty result preserves normal context selection; an error aborts
// initialization.
// The resolver must not initialize the API client.
func WithContextResolver(resolve func(*DockerCli) (string, error)) CLIOption {
return func(cli *DockerCli) error {
cli.contextResolver = resolve
return nil
}
}
// WithAPIClient configures the cli to use the given API client. // WithAPIClient configures the cli to use the given API client.
func WithAPIClient(c client.APIClient) CLIOption { func WithAPIClient(c client.APIClient) CLIOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
-58
View File
@@ -325,64 +325,6 @@ func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
assert.Check(t, cli.ContextStore() != nil) assert.Check(t, cli.ContextStore() != nil)
} }
func TestInitializeContextResolver(t *testing.T) {
resolverErr := errors.New("resolver failed")
for _, tc := range []struct {
name string
context string
err error
}{
{name: "resolved context", context: "resolved"},
{name: "normal selection"},
{name: "resolver failure", err: resolverErr},
} {
t.Run(tc.name, func(t *testing.T) {
originalConfigDir := config.Dir()
t.Cleanup(func() { config.SetDir(originalConfigDir) })
config.SetDir(t.TempDir())
configDir := t.TempDir()
cfg := configfile.New(filepath.Join(configDir, config.ConfigFileName))
cfg.Features = map[string]string{"cloud": "foobar"}
assert.NilError(t, cfg.Save())
cli, err := NewDockerCli()
assert.NilError(t, err)
var loadedConfig *configfile.ConfigFile
calls := 0
err = cli.Initialize(&flags.ClientOptions{ConfigDir: configDir, Context: "original"},
WithContextResolver(func(cli *DockerCli) (string, error) {
calls++
assert.Equal(t, config.Dir(), configDir)
loadedConfig = cli.ConfigFile()
assert.Equal(t, loadedConfig.Features["cloud"], "foobar")
assert.Assert(t, cli.ContextStore() != nil)
assert.Assert(t, cli.client == nil)
return tc.context, tc.err
}),
)
assert.Equal(t, calls, 1)
assert.Assert(t, cli.ConfigFile() == loadedConfig)
assert.Assert(t, cli.client == nil)
if tc.err != nil {
assert.ErrorIs(t, err, tc.err)
return
}
assert.NilError(t, err)
wantContext := tc.context
if wantContext == "" {
wantContext = "original"
}
assert.Equal(t, cli.CurrentContext(), wantContext)
})
}
}
func TestHooksEnabled(t *testing.T) { func TestHooksEnabled(t *testing.T) {
t.Run("disabled by default", func(t *testing.T) { t.Run("disabled by default", func(t *testing.T) {
// Make sure we don't depend on any existing ~/.docker/config.json // Make sure we don't depend on any existing ~/.docker/config.json
+2 -5
View File
@@ -260,11 +260,8 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions)
contextDir = tempDir contextDir = tempDir
case build.ContextTypeRemote: case build.ContextTypeRemote:
buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, options.context, options.dockerfileName) buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, options.context, options.dockerfileName)
if err != nil { if err != nil && options.quiet {
if options.quiet { _, _ = fmt.Fprintln(dockerCli.Err(), progBuff)
_, _ = fmt.Fprintln(dockerCli.Err(), progBuff)
}
return err
} }
default: default:
return fmt.Errorf("unable to prepare context: path %q not found", options.context) return fmt.Errorf("unable to prepare context: path %q not found", options.context)
-31
View File
@@ -6,8 +6,6 @@ import (
"compress/gzip" "compress/gzip"
"context" "context"
"io" "io"
"net/http"
"net/http/httptest"
"os" "os"
"path/filepath" "path/filepath"
"slices" "slices"
@@ -174,35 +172,6 @@ RUN echo hello world
assert.DeepEqual(t, fakeBuild.filenames(t), []string{"Dockerfile"}) assert.DeepEqual(t, fakeBuild.filenames(t), []string{"Dockerfile"})
} }
// TestRunBuildRemoteContextDownloadError verifies that remote context
// download errors are propagated.
//
// regression test for https://github.com/docker/cli/issues/7372
func TestRunBuildRemoteContextDownloadError(t *testing.T) {
t.Setenv("DOCKER_BUILDKIT", "0")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "not found", http.StatusNotFound)
}))
defer server.Close()
imageBuildCalled := false
cli := test.NewFakeCli(&fakeClient{
imageBuildFunc: func(ctx context.Context, buildContext io.Reader, options client.ImageBuildOptions) (client.ImageBuildResult, error) {
imageBuildCalled = true
return client.ImageBuildResult{Body: io.NopCloser(bytes.NewReader(nil))}, nil
},
})
options := newBuildOptions()
options.context = server.URL
err := runBuild(context.Background(), cli, options)
assert.ErrorContains(t, err, "unable to download remote context")
assert.Assert(t, !imageBuildCalled, "ImageBuild should not be called when downloading the remote context fails")
}
type fakeBuild struct { type fakeBuild struct {
context *tar.Reader context *tar.Reader
options client.ImageBuildOptions options client.ImageBuildOptions
+10 -10
View File
@@ -5,13 +5,13 @@ go 1.26.0
require ( require (
github.com/containerd/errdefs v1.0.0 github.com/containerd/errdefs v1.0.0
github.com/distribution/reference v0.6.0 github.com/distribution/reference v0.6.0
github.com/docker/cli v29.8.2+incompatible github.com/docker/cli v29.7.2+incompatible
github.com/docker/cli-docs-tool v0.11.0 github.com/docker/cli-docs-tool v0.11.0
github.com/docker/distribution v2.8.3+incompatible github.com/docker/distribution v2.8.3+incompatible
github.com/docker/go-connections v0.8.2 github.com/docker/go-connections v0.8.1
github.com/fvbommel/sortorder v1.2.0 github.com/fvbommel/sortorder v1.2.0
github.com/moby/moby/api v1.56.1 github.com/moby/moby/api v1.55.0
github.com/moby/moby/client v0.6.2 github.com/moby/moby/client v0.5.1
github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/go-digest v1.0.0
github.com/sirupsen/logrus v1.10.2 github.com/sirupsen/logrus v1.10.2
github.com/spf13/cobra v1.10.2 github.com/spf13/cobra v1.10.2
@@ -22,7 +22,7 @@ require (
) )
require ( require (
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect
@@ -66,11 +66,11 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.46.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.46.0 // indirect
go.opentelemetry.io/otel/trace v1.46.0 // indirect go.opentelemetry.io/otel/trace v1.46.0 // indirect
go.opentelemetry.io/proto/otlp v1.11.0 // indirect go.opentelemetry.io/proto/otlp v1.11.0 // indirect
golang.org/x/crypto v0.57.0 // indirect golang.org/x/crypto v0.56.0 // indirect
golang.org/x/net v0.60.0 // indirect golang.org/x/net v0.58.0 // indirect
golang.org/x/sys v0.48.0 // indirect golang.org/x/sys v0.47.0 // indirect
golang.org/x/term v0.46.0 // indirect golang.org/x/term v0.45.0 // indirect
golang.org/x/text v0.42.0 // indirect golang.org/x/text v0.41.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
google.golang.org/grpc v1.83.2 // indirect google.golang.org/grpc v1.83.2 // indirect
+21 -20
View File
@@ -1,5 +1,5 @@
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 h1:m2xcjRABtG288vnlV3U/PKh/HnPURxZp50RKNUwBxM8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4/go.mod h1:MTUE7ZuZO89FAtAwozGg6AWD83pcdefnEq5gF82MB8s= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
@@ -42,8 +42,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v29.8.2+incompatible h1:2zgdFuoFst2T80oS42vhKGxkd0JRo305eIEKTsxR7pQ= github.com/docker/cli v29.7.2+incompatible h1:dlkwallR8XqfeVnA2ELEhdwvb4lsSwuB4IgsG8Q9cLY=
github.com/docker/cli v29.8.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v29.7.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli-docs-tool v0.11.0 h1:7d8QARFb7QEobizqxmEM7fOteZEHwH/zWgHQtHZEcfE= github.com/docker/cli-docs-tool v0.11.0 h1:7d8QARFb7QEobizqxmEM7fOteZEHwH/zWgHQtHZEcfE=
github.com/docker/cli-docs-tool v0.11.0/go.mod h1:ma8BKiisUo8D6W05XEYIh3oa1UbgrZhi1nowyKFJa8Q= github.com/docker/cli-docs-tool v0.11.0/go.mod h1:ma8BKiisUo8D6W05XEYIh3oa1UbgrZhi1nowyKFJa8Q=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
@@ -54,8 +54,8 @@ github.com/docker/docker-credential-helpers v0.9.9/go.mod h1:v1S+hepowrQXITkEfw6
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0= github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q= github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-connections v0.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc= github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M=
github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
github.com/docker/go-metrics v0.1.0 h1:r76KPNpstz+IvQKSWpYegSkkyzex0V3A1ZGVx6bhGlY= github.com/docker/go-metrics v0.1.0 h1:r76KPNpstz+IvQKSWpYegSkkyzex0V3A1ZGVx6bhGlY=
github.com/docker/go-metrics v0.1.0/go.mod h1:PciI3sONtB051kXALN1JoIlpcu54E1FuPh+4DuqEzyw= github.com/docker/go-metrics v0.1.0/go.mod h1:PciI3sONtB051kXALN1JoIlpcu54E1FuPh+4DuqEzyw=
@@ -133,10 +133,10 @@ github.com/mitchellh/mapstructure v1.0.0 h1:vVpGvMXJPqSDh2VYHF7gsfQj8Ncx+Xw5Y1KH
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/moby/api v1.56.1 h1:PpWkvVPB7Fr/No8w+TfyJ/I6rWZ2YPZhT3JorIg+06c= github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
github.com/moby/moby/api v1.56.1/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk= github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.6.2 h1:ekSPlMccrZGcw6Scb8T9D39qE0mB+XupU/yS0zNw9N4= github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw=
github.com/moby/moby/client v0.6.2/go.mod h1:DGcnO7qmPJrqobx/ggQgQ/AUk+2wgVdQv7uxB0/mEsc= github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8= github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
@@ -236,26 +236,27 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
-322
View File
@@ -1,322 +0,0 @@
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"github.com/containerd/errdefs"
"github.com/docker/cli/cli"
pluginmanager "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config"
contextdocker "github.com/docker/cli/cli/context/docker"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// processCloud resolves the global --cloud[=NAME] option after configuration is
// loaded, but before telemetry and endpoint initialization.
// The option appears in help and completion only when the selected provider is
// installed, valid, and declares support for the resolver contract.
// The provider defaults to the offload plugin; features.cloud in the effective
// Docker config.json can override the plugin name, not its executable path.
// Explicit use also requires the provider's capability declaration.
// Discovery for visibility does not provision a context or connect to a daemon.
//
// # Provider contract
//
// The provider must include "Features": {"cloud-context-resolver": true} in its
// docker-cli-plugin-metadata response to declare support for this contract.
// Older plugins without this declaration leave the flag hidden and cannot be
// used for resolution.
// The CLI uses normal plugin discovery and invokes:
//
// docker-<provider> --config=<dir> <provider> __resolve-context -- <name>
//
// Bare --cloud and --cloud= pass default as the target name.
// The effective config directory is also passed as DOCKER_CONFIG.
// The provider must provision into that context store without changing the saved
// current context or recursively forwarding --cloud.
// It inherits the environment and sends progress and prompts to stderr. Stdin
// is forwarded only when both stdin and stderr are terminals with file handles;
// wrapped Windows consoles use the corresponding standard handles. Otherwise
// the provider receives EOF and must not prompt or open a terminal
// separately. Piped and redirected input belongs to the requested command.
// Stdout must contain exactly one JSON object:
//
// {"DOCKER_CONTEXT":"provisioned-context"}
//
// The returned context must exist, must not be the virtual default context, and
// must have a Docker endpoint with a nonempty host.
// Resolver failures abort command execution without falling back to another
// context.
// Cancellation terminates the resolver process; provisioning subprocesses and
// resource cleanup remain the provider's responsibility.
// Downstream plugins receive --context=<resolved-name> instead of the global
// --cloud option.
// The provider implementation ships separately and must support this operation
// before enabling the integration.
func processCloud(ctx context.Context, dockerCli *command.DockerCli, cmd *cobra.Command, args, osArgs []string) ([]string, error) {
if !cmd.Flags().Changed("cloud") {
return osArgs, nil
}
if cmd.Flags().Changed("context") || cmd.Flags().Changed("host") {
return osArgs, errors.New("conflicting options: cannot specify --cloud together with --context or --host")
}
help, err := cloudHelpRequest(cmd, args)
if err != nil {
return osArgs, err
}
var contextName string
if !help {
name, _ := cmd.Flags().GetString("cloud")
if name == "" {
name = "default"
}
contextName, err = resolveCloudContext(ctx, dockerCli, cmd, name)
if err != nil {
return osArgs, fmt.Errorf("--cloud: %w", err)
}
if err := cmd.Flags().Set("context", contextName); err != nil {
return osArgs, err
}
}
return cloudPluginArgs(cmd, osArgs, contextName)
}
// cloudResolverFeature is the plugin metadata feature that declares support for
// the __resolve-context contract.
const cloudResolverFeature = "cloud-context-resolver"
const cloudPluginHelp = "to use --cloud, install the plugin. See https://docs.docker.com/go/cloud-flag/"
func cloudProvider(dockerCli config.Provider, rootCmd *cobra.Command) (*pluginmanager.Plugin, error) {
provider := dockerCli.ConfigFile().Features["cloud"]
if provider == "" {
provider = "offload"
}
var help string
if provider == "offload" {
help = "\n\n" + cloudPluginHelp
}
plugin, err := pluginmanager.GetPlugin(provider, dockerCli, rootCmd)
if err != nil {
if provider == "offload" && errdefs.IsNotFound(err) {
return nil, errors.New(cloudPluginHelp)
}
return nil, fmt.Errorf("plugin %q unavailable: %w%s", provider, err, help)
}
if plugin.Err != nil {
return nil, fmt.Errorf("invalid plugin %q: %w%s", provider, plugin.Err, help)
}
if supported, _ := plugin.Features[cloudResolverFeature].(bool); !supported {
return nil, fmt.Errorf("plugin %q does not support --cloud%s", provider, help)
}
return plugin, nil
}
// updateCloudFlagVisibility runs only for help and completion, avoiding plugin
// discovery on ordinary invocations that do not use --cloud.
func updateCloudFlagVisibility(dockerCli config.Provider, rootCmd *cobra.Command) {
flag := rootCmd.Flags().Lookup("cloud")
if flag == nil {
return
}
_, err := cloudProvider(dockerCli, rootCmd)
flag.Hidden = err != nil
}
func resolveCloudContext(ctx context.Context, dockerCli *command.DockerCli, rootCmd *cobra.Command, name string) (string, error) {
if err := ctx.Err(); err != nil {
return "", err
}
plugin, err := cloudProvider(dockerCli, rootCmd)
if err != nil {
return "", err
}
cmd := exec.CommandContext(ctx, plugin.Path, "--config="+config.Dir(), plugin.Name, "__resolve-context", "--", name) // #nosec G204 -- executable validated through CLI plugin discovery
cmd.Env = append(os.Environ(), config.EnvOverrideConfigDir+"="+config.Dir(), metadata.ReexecEnvvar+"="+os.Args[0])
setResolverStdio(cmd, dockerCli)
out, err := cmd.Output()
if err != nil {
if ctx.Err() != nil {
return "", ctx.Err()
}
return "", fmt.Errorf("cloud resolver failed: %w", err)
}
var response struct {
DockerContext string `json:"DOCKER_CONTEXT"`
}
if err := json.Unmarshal(out, &response); err != nil {
return "", fmt.Errorf("invalid cloud resolver response: %w", err)
}
response.DockerContext = strings.TrimSpace(response.DockerContext)
if response.DockerContext == "" || response.DockerContext == command.DefaultContextName {
return "", errors.New("cloud resolver must return a non-default DOCKER_CONTEXT")
}
if err := validateResolvedContext(dockerCli, response.DockerContext); err != nil {
return "", err
}
return response.DockerContext, nil
}
// setResolverStdio connects the resolver to the terminal when both stdin and
// stderr are terminals, so the plugin can prompt interactively. Otherwise
// stderr is forwarded and stdin is left unset.
func setResolverStdio(cmd *exec.Cmd, dockerCli *command.DockerCli) {
cmd.Stderr = dockerCli.Err()
stdinTerminal := dockerCli.In().IsTerminal()
stderrTerminal := dockerCli.Err().IsTerminal()
stdin, stdinFile := dockerCli.In().File()
stderr, stderrFile := dockerCli.Err().File()
if runtime.GOOS == "windows" {
// term.StdStreams can wrap console handles for terminal emulation,
// preventing File from exposing them to the child process.
if stdinTerminal && !stdinFile {
stdin, stdinFile = os.Stdin, true
}
if stderrTerminal && !stderrFile {
stderr, stderrFile = os.Stderr, true
}
}
if stdinTerminal && stderrTerminal && stdinFile && stderrFile {
// Pass files directly: wrapping them makes os/exec copy through pipes,
// hiding terminal identity and potentially consuming the command's input.
cmd.Stdin = stdin
cmd.Stderr = stderr
}
}
// validateResolvedContext checks that the context exists and has a Docker
// endpoint host, so a missing context or endpoint cannot fall back to the
// local engine.
func validateResolvedContext(dockerCli *command.DockerCli, name string) error {
meta, err := dockerCli.ContextStore().GetMetadata(name)
if err != nil {
return fmt.Errorf("loading resolved context %q: %w", name, err)
}
endpoint, err := contextdocker.EndpointFromContext(meta)
if err != nil {
return fmt.Errorf("invalid resolved context %q: %w", name, err)
}
if endpoint.Host == "" {
return fmt.Errorf("resolved context %q has no Docker endpoint host", name)
}
return nil
}
// cloudHelpRequest avoids provisioning for help and shell completion.
// Parse known command flags rather than scanning argv: --help may be an option
// value or an argument to a container, not a request for Docker help.
// Unknown flags and missing flag values return a usage error, so a mistyped
// invocation fails without provisioning or falling back to the local engine.
// Flag values are validated only by the command's own parse, after resolution.
func cloudHelpRequest(rootCmd *cobra.Command, args []string) (bool, error) {
help, _ := rootCmd.PersistentFlags().GetBool("help")
version, _ := rootCmd.Flags().GetBool("version")
if help || version || len(args) == 0 {
return true, nil
}
switch args[0] {
case "help", "completion", cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd:
return true, nil
}
cmd, remaining, err := rootCmd.Find(args)
if err != nil || cmd == rootCmd || pluginmanager.IsPluginCommand(cmd) {
// Plugin flags are opaque. Only recognize help immediately after the
// plugin name; deeper help is available through "docker help PLUGIN".
return len(args) > 1 && isHelpFlag(args[1]), nil
}
// "build", "bake", "builder", and "image build" are builtin commands that
// processAliases may still rewrite into a call to the builder plugin
// (buildx) after cloud resolution runs, so their flag set here is only the
// legacy builder's and does not reflect what will actually parse the
// remaining args. Treat them like a plugin command above: only recognize
// help immediately after the resolved command path.
if _, _, _, forwarded := forwardBuilder(builderDefaultPlugin, args, args); forwarded {
return len(remaining) > 0 && isHelpFlag(remaining[0]), nil
}
cmd.InitDefaultHelpFlag()
flags := cmd.Flags()
flags.AddFlagSet(cmd.PersistentFlags())
flags.AddFlagSet(cmd.InheritedFlags())
err = flags.ParseAll(remaining, func(flag *pflag.Flag, value string) error {
if flag.Name == "help" {
var err error
if help, err = strconv.ParseBool(value); err != nil {
return fmt.Errorf("invalid argument %q for \"--help\" flag: %w", value, err)
}
}
return nil
})
if err != nil {
// Format the usage error directly. The root command's FlagErrorFunc
// first checks whether the daemon supports the command, which would
// connect to the engine selected before --cloud is resolved.
return false, cli.FlagErrorFunc(cmd, err)
}
return help, nil
}
// isHelpFlag reports whether arg requests help, in any of the forms pflag
// accepts for a boolean flag.
func isHelpFlag(arg string) bool {
return arg == "--help" || arg == "-h" || arg == "--help=true"
}
// cloudPluginArgs only rewrites the global prefix, leaving subcommand arguments
// untouched. Parsing also distinguishes --cloud from another flag's value.
func cloudPluginArgs(cmd *cobra.Command, osArgs []string, contextName string) ([]string, error) {
flags := pflag.NewFlagSet(cmd.Name(), pflag.ContinueOnError)
flags.SetInterspersed(false)
flags.AddFlagSet(cmd.Flags())
flags.AddFlagSet(cmd.PersistentFlags())
result := []string{osArgs[0]}
if contextName != "" {
result = append(result, "--context="+contextName)
}
if err := flags.ParseAll(osArgs[1:], func(flag *pflag.Flag, value string) error {
if flag.Name != "cloud" {
result = append(result, "--"+flag.Name+"="+value)
}
return nil
}); err != nil {
return osArgs, err
}
if flags.ArgsLenAtDash() >= 0 {
result = append(result, "--")
}
return append(result, flags.Args()...), nil
}
-682
View File
@@ -1,682 +0,0 @@
package main
import (
"bytes"
"cmp"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/creack/pty"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
contextdocker "github.com/docker/cli/cli/context/docker"
"github.com/docker/cli/cli/context/store"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestCloudFlagHidden(t *testing.T) {
dockerCli, err := command.NewDockerCli()
assert.NilError(t, err)
tcmd := newDockerCommand(dockerCli)
tcmd.SetArgs([]string{"--cloud=team", "--help"})
cmd, _, err := tcmd.HandleGlobalFlags()
assert.NilError(t, err)
name, err := cmd.Flags().GetString("cloud")
assert.NilError(t, err)
assert.Equal(t, name, "team")
assert.Assert(t, cmd.Flags().Lookup("cloud").Hidden)
assert.Assert(t, !strings.Contains(cmd.UsageString(), "--cloud"))
}
func TestCloudHelpRequest(t *testing.T) {
for _, tc := range []struct {
name string
args []string
help bool
}{
{name: "no command", help: true},
{name: "global help", args: []string{"--help"}, help: true},
{name: "global version", args: []string{"--version"}, help: true},
{name: "help command", args: []string{"help", "run"}, help: true},
{name: "completion request", args: []string{"__complete", "run", ""}, help: true},
{name: "completion script", args: []string{"completion", "fish"}, help: true},
{name: "builtin help", args: []string{"run", "--help"}, help: true},
{name: "boolean help value", args: []string{"run", "--help=1"}, help: true},
{name: "nested help", args: []string{"container", "run", "--help"}, help: true},
{name: "plugin help", args: []string{"compose", "--help"}, help: true},
{name: "command execution", args: []string{"ps"}},
{name: "container help", args: []string{"run", "alpine", "--help"}},
{name: "flag value", args: []string{"run", "--name", "--help", "alpine"}},
{name: "help disabled", args: []string{"run", "--help=false", "alpine"}},
{name: "build help", args: []string{"build", "--help"}, help: true},
{name: "build buildkit-only flag", args: []string{"build", "--secret", "id=s,src=./s", "."}},
{name: "image build buildkit-only flag", args: []string{"image", "build", "--push", "."}},
{name: "builder build buildkit-only flag", args: []string{"builder", "build", "--ssh", "default", "."}},
{name: "bake help", args: []string{"bake", "--help"}, help: true},
} {
t.Run(tc.name, func(t *testing.T) {
dockerCli, err := command.NewDockerCli()
assert.NilError(t, err)
tcmd := newDockerCommand(dockerCli)
tcmd.SetArgs(append([]string{"--cloud"}, tc.args...))
cmd, args, err := tcmd.HandleGlobalFlags()
assert.NilError(t, err)
help, err := cloudHelpRequest(cmd, args)
assert.NilError(t, err)
assert.Equal(t, help, tc.help)
})
}
}
// Flag errors are reported before cloud resolution, so they must not run the
// command's daemon feature checks against the engine selected before it.
func TestCloudFlagErrorSkipsDaemon(t *testing.T) {
originalConfig := config.Dir()
t.Cleanup(func() { config.SetDir(originalConfig) })
config.SetDir(t.TempDir())
var pings atomic.Int32
daemon := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
pings.Add(1)
w.Header().Set("Docker-Experimental", "false")
}))
defer daemon.Close()
t.Setenv("DOCKER_HOST", "tcp://"+daemon.Listener.Addr().String())
dockerCli, err := command.NewDockerCli()
assert.NilError(t, err)
tcmd := newDockerCommand(dockerCli)
tcmd.SetArgs([]string{"--cloud", "checkpoint", "ls", "--unknown"})
cmd, args, err := tcmd.HandleGlobalFlags()
assert.NilError(t, err)
assert.NilError(t, tcmd.Initialize())
_, err = cloudHelpRequest(cmd, args)
assert.Check(t, is.ErrorContains(err, "unknown flag: --unknown"))
assert.Check(t, is.Equal(pings.Load(), int32(0)))
}
func TestCloudPluginArgs(t *testing.T) {
for _, tc := range []struct {
name string
args []string
context string
want []string
}{
{
name: "global flag only",
args: []string{"docker", "--cloud=foo", "compose", "up", "--cloud=child"},
context: "cloud-foo",
want: []string{"docker", "--context=cloud-foo", "compose", "up", "--cloud=child"},
},
{
name: "flag value is not cloud flag",
args: []string{"docker", "--config", "--cloud", "--cloud", "-D", "compose", "up"},
context: "cloud-default",
want: []string{"docker", "--context=cloud-default", "--config=--cloud", "--debug=true", "compose", "up"},
},
{
name: "delimiter preserved",
args: []string{"docker", "--cloud=foo", "--", "compose", "--", "--cloud"},
context: "cloud-foo",
want: []string{"docker", "--context=cloud-foo", "--", "compose", "--", "--cloud"},
},
{
name: "repeated cloud flags",
args: []string{"docker", "--cloud=foo", "--cloud=bar", "compose", "up"},
context: "cloud-bar",
want: []string{"docker", "--context=cloud-bar", "compose", "up"},
},
{
name: "help without resolution",
args: []string{"docker", "--cloud", "compose", "--help"},
want: []string{"docker", "compose", "--help"},
},
} {
t.Run(tc.name, func(t *testing.T) {
dockerCli, err := command.NewDockerCli()
assert.NilError(t, err)
tcmd := newDockerCommand(dockerCli)
tcmd.SetArgs(tc.args[1:])
cmd, _, err := tcmd.HandleGlobalFlags()
assert.NilError(t, err)
got, err := cloudPluginArgs(cmd, tc.args, tc.context)
assert.NilError(t, err)
assert.DeepEqual(t, got, tc.want)
})
}
}
// Exercise the real startup and subprocess boundary without a daemon or cloud
// service, including failures that must never dispatch the requested command.
func TestCloudResolution(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("fixture plugins use shell scripts")
}
executable, err := os.Executable()
assert.NilError(t, err)
for _, tc := range []struct {
name string
args []string
response string
exit int
wantName string
wantErr string
plugin bool
skip bool
provider string
noProvider bool
legacyProvider bool
invalidProvider bool
wantHelp bool
command []string
visible bool
}{
{
name: "default target",
args: []string{"--cloud"},
wantName: "default",
},
{
name: "empty target",
args: []string{"--cloud="},
wantName: "default",
},
{
name: "named target",
args: []string{"--cloud=team"},
wantName: "team",
},
{
name: "plugin dispatch",
args: []string{"--cloud=team"},
wantName: "team",
plugin: true,
},
{
name: "nonzero exit",
args: []string{"--cloud"},
exit: 1,
wantErr: "cloud resolver failed",
},
{
name: "malformed JSON",
args: []string{"--cloud"},
response: "not JSON",
wantErr: "invalid cloud resolver response",
},
{
name: "multiple responses",
args: []string{"--cloud"},
response: `{} {}`,
wantErr: "invalid cloud resolver response",
},
{
name: "missing field",
args: []string{"--cloud"},
response: `{}`,
wantErr: "must return a non-default DOCKER_CONTEXT",
},
{
name: "local context",
args: []string{"--cloud"},
response: `{"DOCKER_CONTEXT":"default"}`,
wantErr: "must return a non-default DOCKER_CONTEXT",
},
{
name: "missing context",
args: []string{"--cloud"},
response: `{"DOCKER_CONTEXT":"missing"}`,
wantErr: `loading resolved context "missing"`,
},
{
name: "missing endpoint host",
args: []string{"--cloud"},
response: `{"DOCKER_CONTEXT":"empty-host"}`,
wantErr: "has no Docker endpoint host",
},
{
name: "explicit context conflict",
args: []string{"--cloud", "--context=other"},
wantErr: "conflicting options",
skip: true,
},
{
name: "explicit host conflict",
args: []string{"--cloud", "--host=tcp://localhost:2375"},
wantErr: "conflicting options",
skip: true,
},
{
name: "invalid flag before help",
args: []string{"--cloud"},
command: []string{"run", "--unknown", "--help"},
wantErr: "unknown flag: --unknown",
skip: true,
},
{
name: "default provider",
args: []string{"--cloud"},
noProvider: true,
wantName: "default",
},
{
name: "legacy default provider",
args: []string{"--cloud"},
noProvider: true,
legacyProvider: true,
wantErr: `plugin "offload" does not support --cloud`,
wantHelp: true,
skip: true,
},
{
name: "invalid default provider",
args: []string{"--cloud"},
noProvider: true,
invalidProvider: true,
wantErr: `invalid plugin "offload": plugin metadata does not define a vendor`,
wantHelp: true,
skip: true,
},
{
name: "invalid configured provider",
args: []string{"--cloud"},
invalidProvider: true,
wantErr: `invalid plugin "foobar": plugin metadata does not define a vendor`,
skip: true,
},
{
name: "legacy configured provider",
args: []string{"--cloud"},
legacyProvider: true,
wantErr: `plugin "foobar" does not support --cloud`,
skip: true,
},
{
name: "configured provider unavailable",
args: []string{"--cloud"},
provider: "missing",
wantErr: `plugin "missing" unavailable`,
skip: true,
},
{
name: "provider cannot be a path",
args: []string{"--cloud"},
provider: "../foobar",
wantErr: `plugin "../foobar" unavailable`,
skip: true,
},
{
name: "no cloud option",
noProvider: true,
skip: true,
},
{
name: "help",
args: []string{"--cloud", "--help"},
noProvider: true,
skip: true,
},
{
name: "version",
args: []string{"--cloud", "--version"},
noProvider: true,
skip: true,
},
{
name: "help exposes supported provider",
command: []string{"--help"},
noProvider: true,
visible: true,
skip: true,
},
{
name: "help hides legacy provider",
command: []string{"--help"},
noProvider: true,
legacyProvider: true,
skip: true,
},
{
name: "help hides missing provider",
command: []string{"--help"},
provider: "missing",
skip: true,
},
{
name: "completion exposes supported provider",
command: []string{"__complete", "--cl"},
noProvider: true,
visible: true,
skip: true,
},
{
name: "completion hides legacy provider",
command: []string{"__complete", "--cl"},
noProvider: true,
legacyProvider: true,
skip: true,
},
{
name: "completion hides missing provider",
command: []string{"__complete", "--cl"},
provider: "missing",
skip: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
// Explicit --cloud must override inherited endpoint selection.
t.Setenv("DOCKER_CONTEXT", "inherited")
t.Setenv("DOCKER_HOST", "tcp://127.0.0.1:1")
t.Setenv("DOCKER_CLI_HOOKS", "false")
t.Setenv("DOCKER_CONFIG", t.TempDir())
configDir := t.TempDir()
installedProvider := "offload"
if !tc.noProvider {
installedProvider = "foobar"
cfg := configfile.New(filepath.Join(configDir, config.ConfigFileName))
provider := cmp.Or(tc.provider, "foobar")
cfg.Features = map[string]string{"cloud": provider}
assert.NilError(t, cfg.Save())
}
pluginDir := filepath.Join(configDir, "cli-plugins")
assert.NilError(t, os.MkdirAll(pluginDir, 0o755))
response := cmp.Or(tc.response, `{"DOCKER_CONTEXT":"resolved"}`)
providerMetadata := metadata.Metadata{SchemaVersion: "0.1.0", Vendor: "test"}
if tc.invalidProvider {
providerMetadata.Vendor = ""
}
if !tc.legacyProvider {
providerMetadata.Features = map[string]any{cloudResolverFeature: true}
}
pluginMetadata, err := json.Marshal(providerMetadata)
assert.NilError(t, err)
script := fmt.Sprintf(`#!/bin/sh
if [ "$1" = docker-cli-plugin-metadata ]; then
echo '%s'
exit 0
fi
printf '%%s\n' "$@" >> "$DOCKER_CONFIG/resolver-args"
echo provisioning >&2
printf '%%s\n' '%s'
exit %d
`, pluginMetadata, response, tc.exit)
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-"+installedProvider), []byte(script), 0o755))
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-cloudtest"), []byte(`#!/bin/sh
if [ "$1" = docker-cli-plugin-metadata ]; then
echo '{"SchemaVersion":"0.1.0","Vendor":"test"}'
exit 0
fi
printf '%s\n' "$@" > "$CLOUD_TEST_ARGS"
`), 0o755))
dispatchFile := filepath.Join(configDir, "dispatch-args")
t.Setenv("CLOUD_TEST_ARGS", dispatchFile)
contextStore := store.New(filepath.Join(configDir, "contexts"), command.DefaultContextStoreConfig())
assert.NilError(t, contextStore.CreateOrUpdate(store.Metadata{
Name: "resolved",
Endpoints: map[string]any{contextdocker.DockerEndpoint: contextdocker.EndpointMeta{Host: "tcp://127.0.0.1:1"}},
}))
assert.NilError(t, contextStore.CreateOrUpdate(store.Metadata{
Name: "empty-host",
Endpoints: map[string]any{contextdocker.DockerEndpoint: contextdocker.EndpointMeta{}},
}))
args := append([]string{"docker", "--config=" + configDir}, tc.args...)
switch {
case tc.command != nil:
args = append(args, tc.command...)
case tc.plugin || tc.wantErr != "":
args = append(args, "cloudtest", "up", "--cloud=child")
default:
args = append(args, "context", "show")
}
payload, err := json.Marshal(args)
assert.NilError(t, err)
var stdout, stderr bytes.Buffer
cmd := exec.CommandContext(t.Context(), executable, "-test.run=^TestCloudCommandProcess$")
cmd.Env = append(os.Environ(), "CLOUD_TEST_COMMAND="+string(payload))
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err = cmd.Run()
if tc.wantErr != "" {
assert.Assert(t, err != nil)
assert.Check(t, is.Contains(stderr.String(), tc.wantErr))
assert.Equal(t, strings.Contains(stderr.String(), cloudPluginHelp), tc.wantHelp, stderr.String())
_, statErr := os.Stat(dispatchFile)
assert.Assert(t, os.IsNotExist(statErr))
assert.Equal(t, stdout.String(), "")
} else {
assert.NilError(t, err, stderr.String())
if tc.command != nil {
assert.Equal(t, strings.Contains(stdout.String(), "--cloud"), tc.visible, stdout.String())
}
if !tc.skip {
if tc.plugin {
args, err := os.ReadFile(dispatchFile)
assert.NilError(t, err)
assert.Equal(t, string(args), "--context=resolved\n--config="+configDir+"\ncloudtest\nup\n--cloud=child\n")
} else {
assert.Equal(t, stdout.String(), "resolved\n")
}
}
}
invocation, err := os.ReadFile(filepath.Join(configDir, "resolver-args"))
if tc.skip {
assert.Assert(t, os.IsNotExist(err))
} else {
assert.NilError(t, err)
assert.Check(t, is.Contains(stderr.String(), "provisioning"))
name := cmp.Or(tc.wantName, "default")
assert.Equal(t, string(invocation), "--config="+configDir+"\n"+installedProvider+"\n__resolve-context\n--\n"+name+"\n")
}
})
}
}
func TestCloudResolverInput(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("fixture plugins use shell scripts and pseudo-terminals")
}
executable, err := os.Executable()
assert.NilError(t, err)
for _, tc := range []struct {
name string
stdinType string
terminalErr bool
prompt bool
}{
{name: "interactive", stdinType: "terminal", terminalErr: true, prompt: true},
{name: "piped stdin", stdinType: "pipe", terminalErr: true},
{name: "redirected stdin", stdinType: "file", terminalErr: true},
{name: "redirected stderr", stdinType: "terminal"},
{name: "noninteractive", stdinType: "pipe"},
} {
t.Run(tc.name, func(t *testing.T) {
t.Setenv("DOCKER_CLI_HOOKS", "false")
configDir := t.TempDir()
pluginDir := filepath.Join(configDir, "cli-plugins")
assert.NilError(t, os.MkdirAll(pluginDir, 0o755))
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-offload"), []byte(`#!/bin/sh
if [ "$1" = docker-cli-plugin-metadata ]; then
echo '{"SchemaVersion":"0.1.0","Vendor":"test","Features":{"cloud-context-resolver":true}}'
exit 0
fi
if [ -t 0 ]; then
[ -t 2 ] || exit 1
printf 'Continue? ' >&2
IFS= read -r reply || exit 1
printf '%s\n' "$reply" > "$DOCKER_CONFIG/resolver-input"
elif IFS= read -r unexpected; then
echo 'resolver consumed command input' >&2
exit 1
fi
echo '{"DOCKER_CONTEXT":"resolved"}'
`), 0o755))
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-cloudtest"), []byte(`#!/bin/sh
if [ "$1" = docker-cli-plugin-metadata ]; then
echo '{"SchemaVersion":"0.1.0","Vendor":"test"}'
exit 0
fi
IFS= read -r input || exit 1
printf '%s\n' "$input"
`), 0o755))
contextStore := store.New(filepath.Join(configDir, "contexts"), command.DefaultContextStoreConfig())
assert.NilError(t, contextStore.CreateOrUpdate(store.Metadata{
Name: "resolved",
Endpoints: map[string]any{contextdocker.DockerEndpoint: contextdocker.EndpointMeta{Host: "tcp://127.0.0.1:1"}},
}))
terminal, tty, err := pty.Open()
assert.NilError(t, err)
t.Cleanup(func() {
_ = tty.Close()
_ = terminal.Close()
})
const commandInput = "input for the original command\n"
var stdin *os.File
switch tc.stdinType {
case "terminal":
stdin = tty
input := commandInput
if tc.prompt {
input = "yes\n" + input
}
_, err = terminal.WriteString(input)
assert.NilError(t, err)
case "pipe":
var writer *os.File
stdin, writer, err = os.Pipe()
assert.NilError(t, err)
t.Cleanup(func() { _ = stdin.Close() })
t.Cleanup(func() { _ = writer.Close() })
_, err = writer.WriteString(commandInput)
assert.NilError(t, err)
assert.NilError(t, writer.Close())
case "file":
inputPath := filepath.Join(configDir, "command-input")
assert.NilError(t, os.WriteFile(inputPath, []byte(commandInput), 0o600))
stdin, err = os.Open(inputPath)
assert.NilError(t, err)
t.Cleanup(func() { _ = stdin.Close() })
}
payload, err := json.Marshal([]string{"docker", "--config=" + configDir, "--cloud", "cloudtest"})
assert.NilError(t, err)
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
defer cancel()
var stdout, stderr bytes.Buffer
cmd := exec.CommandContext(ctx, executable, "-test.run=^TestCloudCommandProcess$")
cmd.Env = append(os.Environ(), "CLOUD_TEST_COMMAND="+string(payload))
cmd.Stdin = stdin
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmd.WaitDelay = time.Second
if tc.terminalErr {
cmd.Stderr = tty
}
assert.NilError(t, cmd.Run(), stderr.String())
assert.Equal(t, stdout.String(), commandInput)
reply, err := os.ReadFile(filepath.Join(configDir, "resolver-input"))
if tc.prompt {
assert.NilError(t, err)
assert.Equal(t, string(reply), "yes\n")
} else {
assert.Assert(t, os.IsNotExist(err))
}
})
}
}
// Run startup in a separate process because it installs process-wide signal
// handlers that intentionally outlive an individual command.
func TestCloudCommandProcess(t *testing.T) {
payload := os.Getenv("CLOUD_TEST_COMMAND")
if payload == "" {
return
}
assert.NilError(t, json.Unmarshal([]byte(payload), &os.Args))
err := dockerMain(context.Background())
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
}
if err == nil {
os.Exit(0)
}
os.Exit(getExitCode(err))
}
func TestCloudResolutionCancellation(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("fixture plugin uses a shell script")
}
originalConfig := config.Dir()
t.Cleanup(func() { config.SetDir(originalConfig) })
config.SetDir(t.TempDir())
cfg := configfile.New(filepath.Join(config.Dir(), config.ConfigFileName))
cfg.Features = map[string]string{"cloud": "foobar"}
assert.NilError(t, cfg.Save())
pluginDir := filepath.Join(config.Dir(), "cli-plugins")
assert.NilError(t, os.MkdirAll(pluginDir, 0o755))
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-foobar"), []byte(`#!/bin/sh
if [ "$1" = docker-cli-plugin-metadata ]; then
echo '{"SchemaVersion":"0.1.0","Vendor":"test","Features":{"cloud-context-resolver":true}}'
exit 0
fi
exec sleep 30
`), 0o755))
dockerCli, err := command.NewDockerCli()
assert.NilError(t, err)
tcmd := newDockerCommand(dockerCli)
tcmd.SetArgs(strings.Fields("--cloud context show"))
cmd, _, err := tcmd.HandleGlobalFlags()
assert.NilError(t, err)
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
defer cancel()
_, err = resolveCloudContext(ctx, dockerCli, cmd, "default")
assert.ErrorIs(t, err, context.DeadlineExceeded)
}
+4 -24
View File
@@ -178,9 +178,6 @@ func newDockerCommand(dockerCli *command.DockerCli) *cli.TopLevelCommand {
_ = cmd.RegisterFlagCompletionFunc("log-level", completeLogLevels) _ = cmd.RegisterFlagCompletionFunc("log-level", completeLogLevels)
cmd.Flags().BoolP("version", "v", false, "Print version information and quit") cmd.Flags().BoolP("version", "v", false, "Print version information and quit")
cmd.Flags().String("cloud", "", "Use a cloud context, provisioning it if necessary (optionally --cloud=NAME)")
cmd.Flags().Lookup("cloud").NoOptDefVal = "default"
cmd.Flags().Lookup("cloud").Hidden = true
setFlagErrorFunc(dockerCli, cmd) setFlagErrorFunc(dockerCli, cmd)
setupHelpCommand(dockerCli, cmd, helpCmd) setupHelpCommand(dockerCli, cmd, helpCmd)
@@ -253,7 +250,6 @@ func setHelpFunc(dockerCli command.Cli, cmd *cobra.Command) {
ccmd.Println(err) ccmd.Println(err)
return return
} }
updateCloudFlagVisibility(dockerCli, ccmd.Root())
if len(args) >= 1 { if len(args) >= 1 {
err := tryRunPluginHelp(dockerCli, ccmd, args) err := tryRunPluginHelp(dockerCli, ccmd, args)
@@ -512,18 +508,7 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
return err return err
} }
if err := tcmd.Initialize( if err := tcmd.Initialize(command.WithEnableGlobalMeterProvider(), command.WithEnableGlobalTracerProvider()); err != nil {
command.WithEnableGlobalMeterProvider(),
command.WithEnableGlobalTracerProvider(),
command.WithContextResolver(func(cli *command.DockerCli) (string, error) {
var err error
os.Args, err = processCloud(ctx, cli, cmd, args, os.Args)
if err != nil {
return "", err
}
return cmd.Flags().GetString("context")
}),
); err != nil {
return err return err
} }
@@ -553,7 +538,6 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
if err := pluginmanager.AddPluginCommandStubs(dockerCli, cmd); err != nil { if err := pluginmanager.AddPluginCommandStubs(dockerCli, cmd); err != nil {
return err return err
} }
updateCloudFlagVisibility(dockerCli, cmd)
} }
var subCommand *cobra.Command var subCommand *cobra.Command
@@ -571,15 +555,11 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
return nil return nil
} }
if !errdefs.IsNotFound(err) { if !errdefs.IsNotFound(err) {
// For plugin not found we fall through to
// cmd.Execute() which deals with reporting
// "command not found" in a consistent way.
return err return err
} }
if args[0] == "offload" {
// Report this before cmd.ExecuteContext(): plugin-specific flags such as
// --cpus otherwise fail parsing before the root command's RunE.
return errors.New("docker: to use 'docker offload', install the plugin.\nSee https://docs.docker.com/go/cloud-flag/")
}
// Other missing or invalid plugins fall through to cmd.ExecuteContext(),
// which reports the standard unknown-command error.
} }
} }
+2 -2
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG GO_VERSION=1.26.10 ARG GO_VERSION=1.26.8
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image. # ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository # It must be a supported tag in the docker.io/library/alpine image repository
@@ -10,7 +10,7 @@ ARG ALPINE_VERSION=3.23
# BUILDX_VERSION sets the version of buildx to install in the dev container. # BUILDX_VERSION sets the version of buildx to install in the dev container.
# It must be a valid tag in the docker.io/docker/buildx-bin image repository # It must be a valid tag in the docker.io/docker/buildx-bin image repository
# on Docker Hub. # on Docker Hub.
ARG BUILDX_VERSION=0.38.0 ARG BUILDX_VERSION=0.34.1
FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS golang FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS golang
+1 -1
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG GO_VERSION=1.26.10 ARG GO_VERSION=1.26.8
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image. # ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository # It must be a supported tag in the docker.io/library/alpine image repository
+1 -1
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG GO_VERSION=1.26.10 ARG GO_VERSION=1.26.8
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image. # ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository # It must be a supported tag in the docker.io/library/alpine image repository
-1
View File
@@ -31,7 +31,6 @@ services:
dockerfile: engine/Dockerfile dockerfile: engine/Dockerfile
args: args:
ENGINE_VERSION: ${ENGINE_VERSION:-29} ENGINE_VERSION: ${ENGINE_VERSION:-29}
ENGINE_SOURCE: ${ENGINE_SOURCE:-dind}
privileged: true privileged: true
command: ['--insecure-registry=registry:5000', '--insecure-registry=privateregistry:5001', '--experimental'] command: ['--insecure-registry=registry:5000', '--insecure-registry=privateregistry:5001', '--experimental']
environment: environment:
+1 -1
View File
@@ -112,7 +112,7 @@ func TestRunWithCgroupNamespace(t *testing.T) {
func TestRunUmask(t *testing.T) { func TestRunUmask(t *testing.T) {
environment.SkipIfDaemonNotLinux(t) environment.SkipIfDaemonNotLinux(t)
skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.56")) t.Skip("FIXME: un-skip once e2e tests v29.8.0")
testCases := []struct { testCases := []struct {
name string name string
+1 -9
View File
@@ -3,16 +3,8 @@
# ENGINE_VERSION is the version of the (docker-in-docker) Docker Engine to # ENGINE_VERSION is the version of the (docker-in-docker) Docker Engine to
# test against. # test against.
ARG ENGINE_VERSION=29 ARG ENGINE_VERSION=29
# ENGINE_SOURCE is "dind" or "moby-bin" (moby/moby-bin:master).
ARG ENGINE_SOURCE=dind
FROM docker:${ENGINE_VERSION}-dind AS engine-dind FROM docker:${ENGINE_VERSION}-dind
# moby/moby-bin only has the daemon binaries.
FROM docker:dind AS engine-moby-bin
COPY --link --from=moby/moby-bin:master / /usr/local/bin/
FROM engine-${ENGINE_SOURCE}
# the openssh-client update is needed for security reasons when using docker:23.0-dind, currently maintained as an lts by mirantis # the openssh-client update is needed for security reasons when using docker:23.0-dind, currently maintained as an lts by mirantis
RUN apk --no-cache add openssl openssh-client openssh-server shadow && \ RUN apk --no-cache add openssl openssh-client openssh-server shadow && \
+1 -10
View File
@@ -1,14 +1,5 @@
ARG ENGINE_VERSION ARG ENGINE_VERSION
# ENGINE_SOURCE is "dind" or "moby-bin" (moby/moby-bin:master). FROM docker:${ENGINE_VERSION}-dind
ARG ENGINE_SOURCE=dind
FROM docker:${ENGINE_VERSION}-dind AS engine-dind
# moby/moby-bin only has the daemon binaries.
FROM docker:dind AS engine-moby-bin
COPY --link --from=moby/moby-bin:master / /usr/local/bin/
FROM engine-${ENGINE_SOURCE}
# Trust the tlsregistry CA so dockerd connects without --insecure-registry. # Trust the tlsregistry CA so dockerd connects without --insecure-registry.
COPY registry/certs/ca.crt /usr/local/share/ca-certificates/tlsregistry-ca.crt COPY registry/certs/ca.crt /usr/local/share/ca-certificates/tlsregistry-ca.crt
-5
View File
@@ -42,11 +42,6 @@ setup() {
sh e2e/testdata/registry/certs/gen-certs.sh sh e2e/testdata/registry/certs/gen-certs.sh
fi fi
# ENGINE_VERSION=master tests against moby/moby-bin:master.
if [ "${ENGINE_VERSION:-}" = "master" ]; then
export ENGINE_SOURCE=moby-bin
fi
COMPOSE_PROJECT_NAME=$project COMPOSE_FILE=$file docker compose up --build -d >&2 COMPOSE_PROJECT_NAME=$project COMPOSE_FILE=$file docker compose up --build -d >&2
# Ensure supporting services exist before running tests. If one fails to start, # Ensure supporting services exist before running tests. If one fails to start,
+13 -13
View File
@@ -15,28 +15,28 @@ require (
dario.cat/mergo v1.0.2 dario.cat/mergo v1.0.2
github.com/containerd/errdefs v1.0.0 github.com/containerd/errdefs v1.0.0
github.com/containerd/errdefs/pkg v0.3.0 github.com/containerd/errdefs/pkg v0.3.0
github.com/containerd/log v0.2.0 github.com/containerd/log v0.1.0
github.com/containerd/platforms v1.0.0-rc.5 github.com/containerd/platforms v1.0.0-rc.5
github.com/creack/pty v1.1.24 github.com/creack/pty v1.1.24
github.com/distribution/reference v0.6.0 github.com/distribution/reference v0.6.0
github.com/docker/cli-docs-tool v0.11.0 github.com/docker/cli-docs-tool v0.11.0
github.com/docker/distribution v2.8.3+incompatible github.com/docker/distribution v2.8.3+incompatible
github.com/docker/docker-credential-helpers v0.9.9 github.com/docker/docker-credential-helpers v0.9.9
github.com/docker/go-connections v0.8.2 github.com/docker/go-connections v0.8.1
github.com/docker/go-units v0.5.0 github.com/docker/go-units v0.5.0
github.com/fvbommel/sortorder v1.2.1 github.com/fvbommel/sortorder v1.2.0
github.com/go-jose/go-jose/v4 v4.1.7 github.com/go-jose/go-jose/v4 v4.1.5
github.com/go-viper/mapstructure/v2 v2.5.0 github.com/go-viper/mapstructure/v2 v2.5.0
github.com/gogo/protobuf v1.3.2 github.com/gogo/protobuf v1.3.2
github.com/google/go-cmp v0.7.0 github.com/google/go-cmp v0.7.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/mattn/go-runewidth v0.0.31 github.com/mattn/go-runewidth v0.0.29
github.com/moby/go-archive v0.3.3 github.com/moby/go-archive v0.3.3
github.com/moby/moby/api v1.56.1 github.com/moby/moby/api v1.56.0
github.com/moby/moby/client v0.6.2 github.com/moby/moby/client v0.6.0
github.com/moby/patternmatcher v0.6.1 github.com/moby/patternmatcher v0.6.1
github.com/moby/swarmkit/v2 v2.1.3 github.com/moby/swarmkit/v2 v2.1.2
github.com/moby/sys/atomicwriter v0.1.0 github.com/moby/sys/atomicwriter v0.1.0
github.com/moby/sys/capability v0.4.0 github.com/moby/sys/capability v0.4.0
github.com/moby/sys/sequential v0.7.0 github.com/moby/sys/sequential v0.7.0
@@ -60,7 +60,7 @@ require (
go.opentelemetry.io/otel/sdk v1.46.0 go.opentelemetry.io/otel/sdk v1.46.0
go.opentelemetry.io/otel/sdk/metric v1.46.0 go.opentelemetry.io/otel/sdk/metric v1.46.0
go.opentelemetry.io/otel/trace v1.46.0 go.opentelemetry.io/otel/trace v1.46.0
go.yaml.in/yaml/v3 v3.0.6 go.yaml.in/yaml/v3 v3.0.5
golang.org/x/sync v0.23.0 golang.org/x/sync v0.23.0
golang.org/x/sys v0.48.0 golang.org/x/sys v0.48.0
golang.org/x/term v0.46.0 golang.org/x/term v0.46.0
@@ -70,8 +70,8 @@ require (
) )
require ( require (
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.3 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -87,7 +87,7 @@ require (
github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/mux v1.8.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.20.2 // indirect github.com/klauspost/compress v1.19.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/user v0.4.1 // indirect github.com/moby/sys/user v0.4.1 // indirect
github.com/moby/sys/userns v0.2.1 // indirect github.com/moby/sys/userns v0.2.1 // indirect
@@ -102,7 +102,7 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect
go.opentelemetry.io/proto/otlp v1.11.0 // indirect go.opentelemetry.io/proto/otlp v1.11.0 // indirect
golang.org/x/mod v0.41.0 // indirect golang.org/x/mod v0.41.0 // indirect
golang.org/x/net v0.60.0 // indirect golang.org/x/net v0.59.0 // indirect
golang.org/x/time v0.16.0 // indirect golang.org/x/time v0.16.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
+27 -26
View File
@@ -2,10 +2,10 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 h1:m2xcjRABtG288vnlV3U/PKh/HnPURxZp50RKNUwBxM8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4/go.mod h1:MTUE7ZuZO89FAtAwozGg6AWD83pcdefnEq5gF82MB8s= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.3 h1:M/52tsoyTfwaxajLnXZm7PqkbMGRH8iSWP+8fcg9MQ4= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.3/go.mod h1:ma0QpxizD4fyyzJ1C7MIblymTD2Nxz5Mho/3CepwIZg= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
@@ -18,8 +18,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.2.0 h1:BewD/umNgVnoczglOpX8eRMyEy5t5iPlu5AIpnWDONc= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.2.0/go.mod h1:/M7L7CXKcPTfNC74XzaK+5H5KbO5+4lJVpuVI6vRLoM= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v1.0.0-rc.5 h1:vXd569rDrz8LeMXzAnBsy6LADV5YtsD8oyaRarxdmSU= github.com/containerd/platforms v1.0.0-rc.5 h1:vXd569rDrz8LeMXzAnBsy6LADV5YtsD8oyaRarxdmSU=
github.com/containerd/platforms v1.0.0-rc.5/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= github.com/containerd/platforms v1.0.0-rc.5/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -37,8 +37,8 @@ github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBi
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.9.9 h1:BkydjIgZ46JnDbqyM2p2fc63KMw6y+KHL3Em/2AGJ7w= github.com/docker/docker-credential-helpers v0.9.9 h1:BkydjIgZ46JnDbqyM2p2fc63KMw6y+KHL3Em/2AGJ7w=
github.com/docker/docker-credential-helpers v0.9.9/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/docker/docker-credential-helpers v0.9.9/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-connections v0.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc= github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M=
github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-events v0.1.0 h1:J8VX4H7Ta8mmBhv/K/24x1kosKOrEEnYmGjcdFHTOW0= github.com/docker/go-events v0.1.0 h1:J8VX4H7Ta8mmBhv/K/24x1kosKOrEEnYmGjcdFHTOW0=
github.com/docker/go-events v0.1.0/go.mod h1:jwVwMgySJX4PiukIIRrind1wPGU8QhPa8aTEPkreUj4= github.com/docker/go-events v0.1.0/go.mod h1:jwVwMgySJX4PiukIIRrind1wPGU8QhPa8aTEPkreUj4=
github.com/docker/go-metrics v0.1.0 h1:r76KPNpstz+IvQKSWpYegSkkyzex0V3A1ZGVx6bhGlY= github.com/docker/go-metrics v0.1.0 h1:r76KPNpstz+IvQKSWpYegSkkyzex0V3A1ZGVx6bhGlY=
@@ -49,10 +49,10 @@ github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
github.com/fvbommel/sortorder v1.2.1 h1:ASc85RbthwLEqzuiCk7WQQiopfyGWQiEYZVJ3Qa4A+U= github.com/fvbommel/sortorder v1.2.0 h1:TRIiRiGX+djh3Yf4FVxmWmAcYfIr5dH0NbzJWOSAWZk=
github.com/fvbommel/sortorder v1.2.1/go.mod h1:LbhO04ijZIeUuvz9B9BkI/qYrpZZEn1gWhxv4QjUKVs= github.com/fvbommel/sortorder v1.2.0/go.mod h1:LbhO04ijZIeUuvz9B9BkI/qYrpZZEn1gWhxv4QjUKVs=
github.com/go-jose/go-jose/v4 v4.1.7 h1:Nac/98uT5LPkfbiFuYa8ktf62a2VxwQ4xbq1GK8pV5E= github.com/go-jose/go-jose/v4 v4.1.5 h1:RjgjO2LOtWOJKUC5wpwY9LR3B3vwVAz6JS2YHfYU6eA=
github.com/go-jose/go-jose/v4 v4.1.7/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-jose/go-jose/v4 v4.1.5/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -78,24 +78,24 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.20.2 h1:v5tiZvm1KGLyg5d5oXdXe3lsSPrbEUiKoXGia//I7Qo= github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
github.com/klauspost/compress v1.20.2/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-runewidth v0.0.31 h1:1oCV4ReTlosDBc6rYRw5cL8S/4y8iv5oRl4SJ5IBgwo= github.com/mattn/go-runewidth v0.0.29 h1:3oGF3R/S2N9DQ3ptftzVIvg2eicmojCzlwBEmqEPDfQ=
github.com/mattn/go-runewidth v0.0.31/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= github.com/mattn/go-runewidth v0.0.29/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME= github.com/moby/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME=
github.com/moby/go-archive v0.3.3/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE= github.com/moby/go-archive v0.3.3/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
github.com/moby/moby/api v1.56.1 h1:PpWkvVPB7Fr/No8w+TfyJ/I6rWZ2YPZhT3JorIg+06c= github.com/moby/moby/api v1.56.0 h1:GQzua3NA599ASSIICx0iFgiJeO9YkdDARvQsm23ZZuQ=
github.com/moby/moby/api v1.56.1/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk= github.com/moby/moby/api v1.56.0/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk=
github.com/moby/moby/client v0.6.2 h1:ekSPlMccrZGcw6Scb8T9D39qE0mB+XupU/yS0zNw9N4= github.com/moby/moby/client v0.6.0 h1:AJjEB21QPbXSXjDsZorFBoDZPhMrfbpaPLgSMAW9Bgs=
github.com/moby/moby/client v0.6.2/go.mod h1:DGcnO7qmPJrqobx/ggQgQ/AUk+2wgVdQv7uxB0/mEsc= github.com/moby/moby/client v0.6.0/go.mod h1:OCo00wNRyA3m4lmJ228W3JbyCN4ZNNYjpOXiJydBdcQ=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/swarmkit/v2 v2.1.3 h1:UcwtwkIRjr75qSgUWCTIuzKUzAXlidxsiqVMnyxSF1w= github.com/moby/swarmkit/v2 v2.1.2 h1:1WDZAI6HVYNKdCG4zlXnTAPyLsLwuhRGWlHoOUf5Z6I=
github.com/moby/swarmkit/v2 v2.1.3/go.mod h1:KhkxT93Hm4wHoNUPJ4JRD384pS7OODFGzJWoDRj9bLE= github.com/moby/swarmkit/v2 v2.1.2/go.mod h1:GQ6T0ij2oBbWX10OHwpvK449xfXkQMZ8J+B+eQ4mgp4=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk= github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk=
@@ -180,8 +180,8 @@ go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT7
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.6 h1:zcDfvjXEqTQmpjniUBLc91xYJMB2c4QOFxEF/LCEckM= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.6/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -193,8 +193,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -203,6 +203,7 @@ golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
+3 -3
View File
@@ -26,7 +26,7 @@ func (csiState csiEntryState) Handle(b byte) (s state, e error) {
func (csiState csiEntryState) Transition(s state) error { func (csiState csiEntryState) Transition(s state) error {
csiState.parser.logf("CsiEntry::Transition %s --> %s", csiState.Name(), s.Name()) csiState.parser.logf("CsiEntry::Transition %s --> %s", csiState.Name(), s.Name())
_ = csiState.baseState.Transition(s) csiState.baseState.Transition(s)
switch s { switch s {
case csiState.parser.ground: case csiState.parser.ground:
@@ -34,9 +34,9 @@ func (csiState csiEntryState) Transition(s state) error {
case csiState.parser.csiParam: case csiState.parser.csiParam:
switch { switch {
case sliceContains(csiParams, csiState.parser.context.currentChar): case sliceContains(csiParams, csiState.parser.context.currentChar):
_ = csiState.parser.collectParam() csiState.parser.collectParam()
case sliceContains(intermeds, csiState.parser.context.currentChar): case sliceContains(intermeds, csiState.parser.context.currentChar):
_ = csiState.parser.collectInter() csiState.parser.collectInter()
} }
} }
+3 -2
View File
@@ -16,7 +16,8 @@ func (csiState csiParamState) Handle(b byte) (s state, e error) {
case sliceContains(alphabetics, b): case sliceContains(alphabetics, b):
return csiState.parser.ground, nil return csiState.parser.ground, nil
case sliceContains(csiCollectables, b): case sliceContains(csiCollectables, b):
return csiState, csiState.parser.collectParam() csiState.parser.collectParam()
return csiState, nil
case sliceContains(executors, b): case sliceContains(executors, b):
return csiState, csiState.parser.execute() return csiState, csiState.parser.execute()
} }
@@ -26,7 +27,7 @@ func (csiState csiParamState) Handle(b byte) (s state, e error) {
func (csiState csiParamState) Transition(s state) error { func (csiState csiParamState) Transition(s state) error {
csiState.parser.logf("CsiParam::Transition %s --> %s", csiState.Name(), s.Name()) csiState.parser.logf("CsiParam::Transition %s --> %s", csiState.Name(), s.Name())
_ = csiState.baseState.Transition(s) csiState.baseState.Transition(s)
switch s { switch s {
case csiState.parser.ground: case csiState.parser.ground:
+1 -1
View File
@@ -25,7 +25,7 @@ func (escState escapeIntermediateState) Handle(b byte) (s state, e error) {
func (escState escapeIntermediateState) Transition(s state) error { func (escState escapeIntermediateState) Transition(s state) error {
escState.parser.logf("escapeIntermediateState::Transition %s --> %s", escState.Name(), s.Name()) escState.parser.logf("escapeIntermediateState::Transition %s --> %s", escState.Name(), s.Name())
_ = escState.baseState.Transition(s) escState.baseState.Transition(s)
switch s { switch s {
case escState.parser.ground: case escState.parser.ground:
+1 -1
View File
@@ -29,7 +29,7 @@ func (escState escapeState) Handle(b byte) (s state, e error) {
func (escState escapeState) Transition(s state) error { func (escState escapeState) Transition(s state) error {
escState.parser.logf("Escape::Transition %s --> %s", escState.Name(), s.Name()) escState.parser.logf("Escape::Transition %s --> %s", escState.Name(), s.Name())
_ = escState.baseState.Transition(s) escState.baseState.Transition(s)
switch s { switch s {
case escState.parser.ground: case escState.parser.ground:
+8 -5
View File
@@ -113,13 +113,16 @@ func (ap *AnsiParser) handle(b byte) error {
if newState == nil { if newState == nil {
ap.logf("WARNING: newState is nil") ap.logf("WARNING: newState is nil")
return errors.New("new state of 'nil' is invalid") return errors.New("New state of 'nil' is invalid.")
} }
if newState == ap.currState { if newState != ap.currState {
return nil if err := ap.changeState(newState); err != nil {
return err
}
} }
return ap.changeState(newState)
return nil
} }
func (ap *AnsiParser) changeState(newState state) error { func (ap *AnsiParser) changeState(newState state) error {
@@ -133,7 +136,7 @@ func (ap *AnsiParser) changeState(newState state) error {
// Perform transition action // Perform transition action
if err := ap.currState.Transition(newState); err != nil { if err := ap.currState.Transition(newState); err != nil {
ap.logf("Transition from '%s' to '%s' failed with: '%v'", ap.currState.Name(), newState.Name(), err) ap.logf("Transition from '%s' to '%s' failed with: '%v'", ap.currState.Name(), newState.Name, err)
return err return err
} }
+9 -5
View File
@@ -4,9 +4,9 @@ import (
"strconv" "strconv"
) )
func parseParams(bytes []byte) []string { func parseParams(bytes []byte) ([]string, error) {
var paramBuff []byte paramBuff := make([]byte, 0, 0)
var params []string params := []string{}
for _, v := range bytes { for _, v := range bytes {
if v == ';' { if v == ';' {
@@ -14,7 +14,7 @@ func parseParams(bytes []byte) []string {
// Completed parameter, append it to the list // Completed parameter, append it to the list
s := string(paramBuff) s := string(paramBuff)
params = append(params, s) params = append(params, s)
paramBuff = paramBuff[:0] paramBuff = make([]byte, 0, 0)
} }
} else { } else {
paramBuff = append(paramBuff, v) paramBuff = append(paramBuff, v)
@@ -27,7 +27,11 @@ func parseParams(bytes []byte) []string {
params = append(params, s) params = append(params, s)
} }
return params return params, nil
}
func parseCmd(context ansiContext) (string, error) {
return string(context.currentChar), nil
} }
func getInt(params []string, dflt int) int { func getInt(params []string, dflt int) int {
+9 -7
View File
@@ -15,9 +15,10 @@ func (ap *AnsiParser) collectInter() error {
} }
func (ap *AnsiParser) escDispatch() error { func (ap *AnsiParser) escDispatch() error {
cmd := string(ap.context.currentChar) cmd, _ := parseCmd(*ap.context)
intermeds := ap.context.interBuffer
ap.logf("escDispatch currentChar: %#x", ap.context.currentChar) ap.logf("escDispatch currentChar: %#x", ap.context.currentChar)
ap.logf("escDispatch: %s(%q)", cmd, string(ap.context.interBuffer)) ap.logf("escDispatch: %v(%v)", cmd, intermeds)
switch cmd { switch cmd {
case "D": // IND case "D": // IND
@@ -30,14 +31,14 @@ func (ap *AnsiParser) escDispatch() error {
return err return err
case "M": // RI case "M": // RI
return ap.eventHandler.RI() return ap.eventHandler.RI()
default:
return nil
} }
return nil
} }
func (ap *AnsiParser) csiDispatch() error { func (ap *AnsiParser) csiDispatch() error {
cmd := string(ap.context.currentChar) cmd, _ := parseCmd(*ap.context)
params := parseParams(ap.context.paramBuffer) params, _ := parseParams(ap.context.paramBuffer)
ap.logf("Parsed params: %v with length: %d", params, len(params)) ap.logf("Parsed params: %v with length: %d", params, len(params))
ap.logf("csiDispatch: %v(%v)", cmd, params) ap.logf("csiDispatch: %v(%v)", cmd, params)
@@ -108,8 +109,9 @@ func (ap *AnsiParser) print() error {
return ap.eventHandler.Print(ap.context.currentChar) return ap.eventHandler.Print(ap.context.currentChar)
} }
func (ap *AnsiParser) clear() { func (ap *AnsiParser) clear() error {
ap.context = &ansiContext{} ap.context = &ansiContext{}
return nil
} }
func (ap *AnsiParser) execute() error { func (ap *AnsiParser) execute() error {
+2
View File
@@ -1,5 +1,7 @@
package ansiterm package ansiterm
type stateID int
type state interface { type state interface {
Enter() error Enter() error
Exit() error Exit() error
+10
View File
@@ -1,5 +1,9 @@
package ansiterm package ansiterm
import (
"strconv"
)
func sliceContains(bytes []byte, b byte) bool { func sliceContains(bytes []byte, b byte) bool {
for _, v := range bytes { for _, v := range bytes {
if v == b { if v == b {
@@ -9,3 +13,9 @@ func sliceContains(bytes []byte, b byte) bool {
return false return false
} }
func convertBytesToInteger(bytes []byte) int {
s := string(bytes)
i, _ := strconv.Atoi(s)
return i
}
+98 -1
View File
@@ -5,9 +5,12 @@ package winterm
import ( import (
"fmt" "fmt"
"os" "os"
"strconv"
"strings"
"syscall" "syscall"
"golang.org/x/sys/windows" "github.com/Azure/go-ansiterm"
windows "golang.org/x/sys/windows"
) )
// Windows keyboard constants // Windows keyboard constants
@@ -52,6 +55,100 @@ const (
ENHANCED_KEY = 0x0100 ENHANCED_KEY = 0x0100
) )
type ansiCommand struct {
CommandBytes []byte
Command string
Parameters []string
IsSpecial bool
}
func newAnsiCommand(command []byte) *ansiCommand {
if isCharacterSelectionCmdChar(command[1]) {
// Is Character Set Selection commands
return &ansiCommand{
CommandBytes: command,
Command: string(command),
IsSpecial: true,
}
}
// last char is command character
lastCharIndex := len(command) - 1
ac := &ansiCommand{
CommandBytes: command,
Command: string(command[lastCharIndex]),
IsSpecial: false,
}
// more than a single escape
if lastCharIndex != 0 {
start := 1
// skip if double char escape sequence
if command[0] == ansiterm.ANSI_ESCAPE_PRIMARY && command[1] == ansiterm.ANSI_ESCAPE_SECONDARY {
start++
}
// convert this to GetNextParam method
ac.Parameters = strings.Split(string(command[start:lastCharIndex]), ansiterm.ANSI_PARAMETER_SEP)
}
return ac
}
func (ac *ansiCommand) paramAsSHORT(index int, defaultValue int16) int16 {
if index < 0 || index >= len(ac.Parameters) {
return defaultValue
}
param, err := strconv.ParseInt(ac.Parameters[index], 10, 16)
if err != nil {
return defaultValue
}
return int16(param)
}
func (ac *ansiCommand) String() string {
return fmt.Sprintf("0x%v \"%v\" (\"%v\")",
bytesToHex(ac.CommandBytes),
ac.Command,
strings.Join(ac.Parameters, "\",\""))
}
// isAnsiCommandChar returns true if the passed byte falls within the range of ANSI commands.
// See http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html.
func isAnsiCommandChar(b byte) bool {
switch {
case ansiterm.ANSI_COMMAND_FIRST <= b && b <= ansiterm.ANSI_COMMAND_LAST && b != ansiterm.ANSI_ESCAPE_SECONDARY:
return true
case b == ansiterm.ANSI_CMD_G1 || b == ansiterm.ANSI_CMD_OSC || b == ansiterm.ANSI_CMD_DECPAM || b == ansiterm.ANSI_CMD_DECPNM:
// non-CSI escape sequence terminator
return true
case b == ansiterm.ANSI_CMD_STR_TERM || b == ansiterm.ANSI_BEL:
// String escape sequence terminator
return true
}
return false
}
func isXtermOscSequence(command []byte, current byte) bool {
return (len(command) >= 2 && command[0] == ansiterm.ANSI_ESCAPE_PRIMARY && command[1] == ansiterm.ANSI_CMD_OSC && current != ansiterm.ANSI_BEL)
}
func isCharacterSelectionCmdChar(b byte) bool {
return (b == ansiterm.ANSI_CMD_G0 || b == ansiterm.ANSI_CMD_G1 || b == ansiterm.ANSI_CMD_G2 || b == ansiterm.ANSI_CMD_G3)
}
// bytesToHex converts a slice of bytes to a human-readable string.
func bytesToHex(b []byte) string {
hex := make([]string, len(b))
for i, ch := range b {
hex[i] = fmt.Sprintf("%X", ch)
}
return strings.Join(hex, "")
}
// ensureInRange adjusts the passed value, if necessary, to ensure it is within // ensureInRange adjusts the passed value, if necessary, to ensure it is within
// the passed min / max range. // the passed min / max range.
func ensureInRange(n int16, min int16, max int16) int16 { func ensureInRange(n int16, min int16, max int16) int16 {
+120 -146
View File
@@ -1,15 +1,9 @@
version: "2"
formatters:
enable:
- gofmt # files are gofmt'ed
linters: linters:
enable: enable:
# style # style
- containedctx # struct contains a context - containedctx # struct contains a context
- dupl # duplicate code - dupl # duplicate code
- dupword # duplicate words in comments - errname # erorrs are named correctly
- errname # errors are named correctly
- intrange # integer ranges in for loop
- nolintlint # "//nolint" directives are properly explained - nolintlint # "//nolint" directives are properly explained
- revive # golint replacement - revive # golint replacement
- unconvert # unnecessary conversions - unconvert # unnecessary conversions
@@ -19,155 +13,135 @@ linters:
- contextcheck # function uses a non-inherited context - contextcheck # function uses a non-inherited context
- errorlint # errors not wrapped for 1.13 - errorlint # errors not wrapped for 1.13
- exhaustive # check exhaustiveness of enum switch statements - exhaustive # check exhaustiveness of enum switch statements
- gocritic # various code quality checks - gofmt # files are gofmt'ed
- gosec # security - gosec # security
- mirror # avoid allocations
- nilerr # returns nil even with non-nil error - nilerr # returns nil even with non-nil error
- thelper # test helpers without t.Helper() - thelper # test helpers without t.Helper()
- unparam # unused function params - unparam # unused function params
exclusions: issues:
paths: exclude-dirs:
- pkg/etw/sample - pkg/etw/sample
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
# err is very often shadowed in nested scopes
- linters:
- govet
text: '^shadow: declaration of "err" shadows declaration'
# ignore long lines for skip autogen directives exclude-rules:
- linters: # err is very often shadowed in nested scopes
- revive - linters:
text: "^line-length-limit: " - govet
source: "^//(go:generate|sys) " text: '^shadow: declaration of "err" shadows declaration'
# ignore comment spacing for sys directives # ignore long lines for skip autogen directives
- linters: - linters:
- revive - revive
text: "^comment-spacings: no space between comment delimiter and comment text" text: "^line-length-limit: "
source: "//(cspell:|sys )" source: "^//(go:generate|sys) "
# allow unjustified ignores of error checks in defer statements #TODO: remove after upgrading to go1.18
- linters: # ignore comment spacing for nolint and sys directives
- nolintlint - linters:
text: "^directive `//nolint:errcheck` should provide explanation" - revive
source: '^\s*defer ' text: "^comment-spacings: no space between comment delimiter and comment text"
source: "//(cspell:|nolint:|sys |todo)"
# allow unjustified ignores of error lints for io.EOF # not on go 1.18 yet, so no any
- linters: - linters:
- nolintlint - revive
text: "^directive `//nolint:errorlint` should provide explanation" text: "^use-any: since GO 1.18 'interface{}' can be replaced by 'any'"
source: '[=|!]= io.EOF'
- linters: # allow unjustified ignores of error checks in defer statements
- gosec - linters:
text: "^G115: integer overflow conversion" - nolintlint
text: "^directive `//nolint:errcheck` should provide explanation"
source: '^\s*defer '
# allow unjustified ignores of error lints for io.EOF
- linters:
- nolintlint
text: "^directive `//nolint:errorlint` should provide explanation"
source: '[=|!]= io.EOF'
settings: linters-settings:
exhaustive: exhaustive:
default-signifies-exhaustive: true default-signifies-exhaustive: true
gocritic: govet:
disable-all: true enable-all: true
enabled-checks: disable:
- nilValReturn # struct order is often for Win32 compat
- sprintfQuotedString # also, ignore pointer bytes/GC issues for now until performance becomes an issue
govet: - fieldalignment
enable-all: true nolintlint:
disable: require-explanation: true
# struct order is often for Win32 compat require-specific: true
# also, ignore pointer bytes/GC issues for now until performance becomes an issue revive:
- fieldalignment # revive is more configurable than static check, so likely the preferred alternative to static-check
nolintlint: # (once the perf issue is solved: https://github.com/golangci/golangci-lint/issues/2997)
require-explanation: true enable-all-rules:
require-specific: true true
revive:
# revive is more configurable than static check, so likely the preferred alternative to static-check
# (once the perf issue is solved: https://github.com/golangci/golangci-lint/issues/2997)
enable-all-rules:
true
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md
rules: rules:
# rules with required arguments # rules with required arguments
- name: argument-limit - name: argument-limit
disabled: true disabled: true
- name: banned-characters - name: banned-characters
disabled: true disabled: true
- name: cognitive-complexity - name: cognitive-complexity
disabled: true disabled: true
- name: cyclomatic - name: cyclomatic
disabled: true disabled: true
# covered by "exhaustive" linter - name: file-header
- name: enforce-switch-style disabled: true
disabled: true - name: function-length
- name: file-header disabled: true
disabled: true - name: function-result-limit
- name: function-length disabled: true
disabled: true - name: max-public-structs
- name: function-result-limit disabled: true
disabled: true # geneally annoying rules
- name: max-public-structs - name: add-constant # complains about any and all strings and integers
disabled: true disabled: true
# generally annoying rules - name: confusing-naming # we frequently use "Foo()" and "foo()" together
- name: add-constant # complains about any and all strings and integers disabled: true
disabled: true - name: flag-parameter # excessive, and a common idiom we use
- name: confusing-naming # we frequently use "Foo()" and "foo()" together disabled: true
disabled: true - name: unhandled-error # warns over common fmt.Print* and io.Close; rely on errcheck instead
- name: flag-parameter # excessive, and a common idiom we use disabled: true
disabled: true # general config
- name: multiline-if-init # too opinionated; see https://github.com/microsoft/go-winio/pull/325#issuecomment-5822487436 - name: line-length-limit
disabled: true arguments:
- name: unhandled-error # warns over common fmt.Print* and io.Close; rely on errcheck instead - 140
disabled: true - name: var-naming
# general config arguments:
- name: line-length-limit - []
arguments: - - CID
- 140 - CRI
- name: var-naming - CTRD
arguments: - DACL
- [] - DLL
- - CID - DOS
- CRI - ETW
- CTRD - FSCTL
- DACL - GCS
- DLL - GMSA
- DOS - HCS
- ETW - HV
- FSCTL - IO
- GCS - LCOW
- GMSA - LDAP
- HCS - LPAC
- HV - LTSC
- IO - MMIO
- LCOW - NT
- LDAP - OCI
- LPAC - PMEM
- LTSC - PWSH
- MMIO - RX
- NT - SACl
- OCI - SID
- PMEM - SMB
- PWSH - TX
- RX - VHD
- SACl - VHDX
- SID - VMID
- SMB - VPCI
- TX - WCOW
- VHD - WIM
- VHDX
- VMID
- VPCI
- WCOW
- WIM
staticcheck:
# Enable all options, with some exceptions.
# For defaults, see https://golangci-lint.run/usage/linters/#staticcheck
checks:
- all
- -QF1008 # Omit embedded fields from selector expression; https://staticcheck.dev/docs/checks/#QF1008
- -ST1003 # Poorly chosen identifier; https://staticcheck.dev/docs/checks/#ST1003
+2 -1
View File
@@ -1,4 +1,5 @@
//go:build windows //go:build windows
// +build windows
package winio package winio
@@ -35,7 +36,7 @@ const (
StreamSparseAttributes = uint32(8) StreamSparseAttributes = uint32(8)
) )
//nolint:revive,nolintlint // var-naming: ALL_CAPS //nolint:revive // var-naming: ALL_CAPS
const ( const (
WRITE_DAC = windows.WRITE_DAC WRITE_DAC = windows.WRITE_DAC
WRITE_OWNER = windows.WRITE_OWNER WRITE_OWNER = windows.WRITE_OWNER
+1 -1
View File
@@ -95,7 +95,7 @@ func writeEa(buf *bytes.Buffer, ea *ExtendedAttribute, last bool) error {
return err return err
} }
_, err = buf.WriteString(ea.Name) _, err = buf.Write([]byte(ea.Name))
if err != nil { if err != nil {
return err return err
} }
+12 -34
View File
@@ -1,11 +1,11 @@
//go:build windows //go:build windows
// +build windows
package winio package winio
import ( import (
"context" "errors"
"io" "io"
"os"
"runtime" "runtime"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -21,33 +21,16 @@ import (
//sys setFileCompletionNotificationModes(h windows.Handle, flags uint8) (err error) = SetFileCompletionNotificationModes //sys setFileCompletionNotificationModes(h windows.Handle, flags uint8) (err error) = SetFileCompletionNotificationModes
//sys wsaGetOverlappedResult(h windows.Handle, o *windows.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult //sys wsaGetOverlappedResult(h windows.Handle, o *windows.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult
// ErrFileClosed is returned when an operation is performed on a closed file. var (
// ErrFileClosed = errors.New("file has already been closed")
// Deprecated: use [os.ErrClosed] instead. ErrTimeout = &timeoutError{}
var ErrFileClosed = os.ErrClosed )
// ErrTimeout is returned when an I/O operation exceeds its deadline.
//
// It matches both [context.DeadlineExceeded] and [os.ErrDeadlineExceeded]
// with [errors.Is].
var ErrTimeout error = &timeoutError{}
// timeoutError preserves the historical "i/o timeout" error while matching
// both context.DeadlineExceeded and os.ErrDeadlineExceeded through errors.Is.
// This mirrors the behavior of net.errTimeout:
// https://github.com/golang/go/blob/go1.27.1/src/net/net.go
//
// TODO: consider replacing ErrTimeout with os.ErrDeadlineExceeded if os.ErrDeadlineExceeded is changed to match context.DeadlineExceeded.
type timeoutError struct{} type timeoutError struct{}
func (*timeoutError) Error() string { return "i/o timeout" } func (*timeoutError) Error() string { return "i/o timeout" }
func (*timeoutError) Timeout() bool { return true } func (*timeoutError) Timeout() bool { return true }
func (*timeoutError) Temporary() bool { return true } func (*timeoutError) Temporary() bool { return true }
func (*timeoutError) Unwrap() error { return os.ErrDeadlineExceeded }
func (*timeoutError) Is(err error) bool {
return err == context.DeadlineExceeded
}
type timeoutChan chan struct{} type timeoutChan chan struct{}
@@ -161,7 +144,7 @@ func (f *win32File) prepareIO() (*ioOperation, error) {
f.wgLock.RLock() f.wgLock.RLock()
if f.closing.Load() { if f.closing.Load() {
f.wgLock.RUnlock() f.wgLock.RUnlock()
return nil, os.ErrClosed return nil, ErrFileClosed
} }
f.wg.Add(1) f.wg.Add(1)
f.wgLock.RUnlock() f.wgLock.RUnlock()
@@ -210,7 +193,7 @@ func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, er
err = r.err err = r.err
if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
if f.closing.Load() { if f.closing.Load() {
err = os.ErrClosed err = ErrFileClosed
} }
} else if err != nil && f.socket { } else if err != nil && f.socket {
// err is from Win32. Query the overlapped structure to get the winsock error. // err is from Win32. Query the overlapped structure to get the winsock error.
@@ -252,17 +235,12 @@ func (f *win32File) Read(b []byte) (int, error) {
runtime.KeepAlive(b) runtime.KeepAlive(b)
// Handle EOF conditions. // Handle EOF conditions.
switch err { if err == nil && n == 0 && len(b) != 0 {
case windows.ERROR_BROKEN_PIPE: //nolint:errorlint // err is Errno return 0, io.EOF
} else if err == windows.ERROR_BROKEN_PIPE { //nolint:errorlint // err is Errno
return 0, io.EOF return 0, io.EOF
case nil:
if n == 0 && len(b) != 0 {
return 0, io.EOF
}
return n, nil
default:
return n, err
} }
return n, err
} }
// Write writes to a file handle. // Write writes to a file handle.
+1
View File
@@ -1,4 +1,5 @@
//go:build windows //go:build windows
// +build windows
package winio package winio
+2 -1
View File
@@ -1,4 +1,5 @@
//go:build windows //go:build windows
// +build windows
package winio package winio
@@ -441,7 +442,7 @@ func canRedial(err error) bool {
func (conn *HvsockConn) opErr(op string, err error) error { func (conn *HvsockConn) opErr(op string, err error) error {
// translate from "file closed" to "socket closed" // translate from "file closed" to "socket closed"
if errors.Is(err, os.ErrClosed) { if errors.Is(err, ErrFileClosed) {
err = socket.ErrSocketClosed err = socket.ErrSocketClosed
} }
return &net.OpError{Op: op, Net: "hvsock", Source: &conn.local, Addr: &conn.remote, Err: err} return &net.OpError{Op: op, Net: "hvsock", Source: &conn.local, Addr: &conn.remote, Err: err}
+1 -1
View File
@@ -243,7 +243,7 @@ const (
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew
func GetFinalPathNameByHandle(h windows.Handle, flags GetFinalPathFlag) (string, error) { func GetFinalPathNameByHandle(h windows.Handle, flags GetFinalPathFlag) (string, error) {
b := stringbuffer.NewWString() b := stringbuffer.NewWString()
// TODO: can loop infinitely if Win32 keeps returning the same (or a larger) n? //TODO: can loop infinitely if Win32 keeps returning the same (or a larger) n?
for { for {
n, err := windows.GetFinalPathNameByHandle(h, b.Pointer(), b.Cap(), uint32(flags)) n, err := windows.GetFinalPathNameByHandle(h, b.Pointer(), b.Cap(), uint32(flags))
if err != nil { if err != nil {
+6 -2
View File
@@ -15,8 +15,12 @@ var _ unsafe.Pointer
// Do the interface allocations only once for common // Do the interface allocations only once for common
// Errno values. // Errno values.
const (
errnoERROR_IO_PENDING = 997
)
var ( var (
errERROR_IO_PENDING error = windows.ERROR_IO_PENDING errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
errERROR_EINVAL error = syscall.EINVAL errERROR_EINVAL error = syscall.EINVAL
) )
@@ -26,7 +30,7 @@ func errnoErr(e syscall.Errno) error {
switch e { switch e {
case 0: case 0:
return errERROR_EINVAL return errERROR_EINVAL
case windows.ERROR_IO_PENDING: case errnoERROR_IO_PENDING:
return errERROR_IO_PENDING return errERROR_IO_PENDING
} }
return e return e
+6 -2
View File
@@ -15,8 +15,12 @@ var _ unsafe.Pointer
// Do the interface allocations only once for common // Do the interface allocations only once for common
// Errno values. // Errno values.
const (
errnoERROR_IO_PENDING = 997
)
var ( var (
errERROR_IO_PENDING error = windows.ERROR_IO_PENDING errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
errERROR_EINVAL error = syscall.EINVAL errERROR_EINVAL error = syscall.EINVAL
) )
@@ -26,7 +30,7 @@ func errnoErr(e syscall.Errno) error {
switch e { switch e {
case 0: case 0:
return errERROR_EINVAL return errERROR_EINVAL
case windows.ERROR_IO_PENDING: case errnoERROR_IO_PENDING:
return errERROR_IO_PENDING return errERROR_IO_PENDING
} }
return e return e
+2 -2
View File
@@ -7,7 +7,7 @@ import (
// TODO: worth exporting and using in mkwinsyscall? // TODO: worth exporting and using in mkwinsyscall?
// MinWStringCap is the buffer size in the pool, chosen somewhat arbitrarily to accommodate // Uint16BufferSize is the buffer size in the pool, chosen somewhat arbitrarily to accommodate
// large path strings: // large path strings:
// MAX_PATH (260) + size of volume GUID prefix (49) + null terminator = 310. // MAX_PATH (260) + size of volume GUID prefix (49) + null terminator = 310.
const MinWStringCap = 310 const MinWStringCap = 310
@@ -16,7 +16,7 @@ const MinWStringCap = 310
// is copied to heap and then referenced via pointer in the interface header that sync.Pool // is copied to heap and then referenced via pointer in the interface header that sync.Pool
// stores. // stores.
var pathPool = sync.Pool{ // if go1.18+ adds Pool[T], use that to store []uint16 directly var pathPool = sync.Pool{ // if go1.18+ adds Pool[T], use that to store []uint16 directly
New: func() any { New: func() interface{} {
b := make([]uint16, MinWStringCap) b := make([]uint16, MinWStringCap)
return &b return &b
}, },
+30 -31
View File
@@ -1,4 +1,5 @@
//go:build windows //go:build windows
// +build windows
package winio package winio
@@ -10,7 +11,6 @@ import (
"net" "net"
"os" "os"
"runtime" "runtime"
"sync"
"time" "time"
"unsafe" "unsafe"
@@ -102,10 +102,12 @@ func (status ntStatus) Err() error {
return rtlNtStatusToDosError(status) return rtlNtStatusToDosError(status)
} }
// ErrPipeListenerClosed is returned for pipe operations on listeners that have been closed. var (
// // ErrPipeListenerClosed is returned for pipe operations on listeners that have been closed.
// Deprecated: use [net.ErrClosed] instead. ErrPipeListenerClosed = net.ErrClosed
var ErrPipeListenerClosed = net.ErrClosed
errPipeWriteClosed = errors.New("pipe has been closed for write")
)
type win32Pipe struct { type win32Pipe struct {
*win32File *win32File
@@ -142,10 +144,9 @@ func (f *win32Pipe) Disconnect() error {
} }
// CloseWrite closes the write side of a message pipe in byte mode. // CloseWrite closes the write side of a message pipe in byte mode.
// It returns [io.ErrClosedPipe] if the write side is already closed.
func (f *win32MessageBytePipe) CloseWrite() error { func (f *win32MessageBytePipe) CloseWrite() error {
if f.writeClosed { if f.writeClosed {
return io.ErrClosedPipe return errPipeWriteClosed
} }
err := f.win32File.Flush() err := f.win32File.Flush()
if err != nil { if err != nil {
@@ -160,10 +161,10 @@ func (f *win32MessageBytePipe) CloseWrite() error {
} }
// Write writes bytes to a message pipe in byte mode. Zero-byte writes are ignored, since // Write writes bytes to a message pipe in byte mode. Zero-byte writes are ignored, since
// they are used to implement CloseWrite. It returns [io.ErrClosedPipe] if the write side is closed. // they are used to implement CloseWrite().
func (f *win32MessageBytePipe) Write(b []byte) (int, error) { func (f *win32MessageBytePipe) Write(b []byte) (int, error) {
if f.writeClosed { if f.writeClosed {
return 0, io.ErrClosedPipe return 0, errPipeWriteClosed
} }
if len(b) == 0 { if len(b) == 0 {
return 0, nil return 0, nil
@@ -178,15 +179,14 @@ func (f *win32MessageBytePipe) Read(b []byte) (int, error) {
return 0, io.EOF return 0, io.EOF
} }
n, err := f.win32File.Read(b) n, err := f.win32File.Read(b)
switch err { if err == io.EOF { //nolint:errorlint
case io.EOF: //nolint:errorlint // error is not wrapped.
// If this was the result of a zero-byte read, then // If this was the result of a zero-byte read, then
// it is possible that the read was due to a zero-size // it is possible that the read was due to a zero-size
// message. Since we are simulating CloseWrite with a // message. Since we are simulating CloseWrite with a
// zero-byte message, ensure that all future Read() calls // zero-byte message, ensure that all future Read() calls
// also return EOF. // also return EOF.
f.readEOF = true f.readEOF = true
case windows.ERROR_MORE_DATA: } else if err == windows.ERROR_MORE_DATA { //nolint:errorlint // err is Errno
// ERROR_MORE_DATA indicates that the pipe's read mode is message mode // ERROR_MORE_DATA indicates that the pipe's read mode is message mode
// and the message still has more bytes. Treat this as a success, since // and the message still has more bytes. Treat this as a success, since
// this package presents all named pipes as byte streams. // this package presents all named pipes as byte streams.
@@ -316,9 +316,8 @@ type win32PipeListener struct {
path string path string
config PipeConfig config PipeConfig
acceptCh chan (chan acceptResponse) acceptCh chan (chan acceptResponse)
closeOnce sync.Once closeCh chan int
closeCh chan struct{} // closed (never sent on) to broadcast listener shutdown doneCh chan int
doneCh chan struct{}
} }
func makeServerPipeHandle(path string, sd []byte, c *PipeConfig, first bool) (windows.Handle, error) { func makeServerPipeHandle(path string, sd []byte, c *PipeConfig, first bool) (windows.Handle, error) {
@@ -345,7 +344,7 @@ func makeServerPipeHandle(path string, sd []byte, c *PipeConfig, first bool) (wi
// The security descriptor is only needed for the first pipe. // The security descriptor is only needed for the first pipe.
if first { if first {
if sd != nil { if sd != nil {
// todo: does `sdb` need to be allocated on the heap, or can go allocate it? //todo: does `sdb` need to be allocated on the heap, or can go allocate it?
l := uint32(len(sd)) l := uint32(len(sd))
sdb, err := windows.LocalAlloc(0, l) sdb, err := windows.LocalAlloc(0, l)
if err != nil { if err != nil {
@@ -446,14 +445,13 @@ func (l *win32PipeListener) makeConnectedServerPipe() (*win32File, error) {
p = nil p = nil
} }
case <-l.closeCh: case <-l.closeCh:
// Abort the connect request by closing the handle. Listener closure is // Abort the connect request by closing the handle.
// authoritative: ConnectNamedPipe may race the handle close and report a p.Close()
// connection or error (e.g. ERROR_NO_DATA) instead of ErrFileClosed, and
// that result must not be surfaced or cause listenerRoutine to retry.
_ = p.Close()
p = nil p = nil
<-ch err = <-ch
err = net.ErrClosed if err == nil || err == ErrFileClosed { //nolint:errorlint // err is Errno
err = ErrPipeListenerClosed
}
} }
return p, err return p, err
} }
@@ -478,7 +476,7 @@ func (l *win32PipeListener) listenerRoutine() {
} }
} }
responseCh <- acceptResponse{p, err} responseCh <- acceptResponse{p, err}
closed = err == net.ErrClosed //nolint:errorlint // err is Errno closed = err == ErrPipeListenerClosed //nolint:errorlint // err is Errno
} }
} }
windows.Close(l.firstHandle) windows.Close(l.firstHandle)
@@ -532,8 +530,8 @@ func ListenPipe(path string, c *PipeConfig) (net.Listener, error) {
path: path, path: path,
config: *c, config: *c,
acceptCh: make(chan (chan acceptResponse)), acceptCh: make(chan (chan acceptResponse)),
closeCh: make(chan struct{}), closeCh: make(chan int),
doneCh: make(chan struct{}), doneCh: make(chan int),
} }
go l.listenerRoutine() go l.listenerRoutine()
return l, nil return l, nil
@@ -570,15 +568,16 @@ func (l *win32PipeListener) Accept() (net.Conn, error) {
} }
return &win32Pipe{win32File: response.f, path: l.path}, nil return &win32Pipe{win32File: response.f, path: l.path}, nil
case <-l.doneCh: case <-l.doneCh:
return nil, net.ErrClosed return nil, ErrPipeListenerClosed
} }
} }
func (l *win32PipeListener) Close() error { func (l *win32PipeListener) Close() error {
l.closeOnce.Do(func() { select {
close(l.closeCh) case l.closeCh <- 1:
}) <-l.doneCh
<-l.doneCh case <-l.doneCh:
}
return nil return nil
} }
+1 -1
View File
@@ -14,7 +14,7 @@ import (
"strconv" "strconv"
) )
//go:generate go tool stringer -type=Variant -trimprefix=Variant -linecomment //go:generate go run golang.org/x/tools/cmd/stringer -type=Variant -trimprefix=Variant -linecomment
// Variant specifies which GUID variant (or "type") of the GUID. It determines // Variant specifies which GUID variant (or "type") of the GUID. It determines
// how the entirety of the rest of the GUID is interpreted. // how the entirety of the rest of the GUID is interpreted.
+1
View File
@@ -1,4 +1,5 @@
//go:build !windows //go:build !windows
// +build !windows
package guid package guid
+1
View File
@@ -1,4 +1,5 @@
//go:build windows //go:build windows
// +build windows
package guid package guid
+2 -3
View File
@@ -20,9 +20,8 @@ const _Variant_name = "UnknownNCSRFC 4122MicrosoftFuture"
var _Variant_index = [...]uint8{0, 7, 10, 18, 27, 33} var _Variant_index = [...]uint8{0, 7, 10, 18, 27, 33}
func (i Variant) String() string { func (i Variant) String() string {
idx := int(i) - 0 if i >= Variant(len(_Variant_index)-1) {
if i < 0 || idx >= len(_Variant_index)-1 {
return "Variant(" + strconv.FormatInt(int64(i), 10) + ")" return "Variant(" + strconv.FormatInt(int64(i), 10) + ")"
} }
return _Variant_name[_Variant_index[idx]:_Variant_index[idx+1]] return _Variant_name[_Variant_index[i]:_Variant_index[i+1]]
} }
+1
View File
@@ -1,4 +1,5 @@
//go:build windows //go:build windows
// +build windows
package winio package winio
+6 -55
View File
@@ -1,11 +1,11 @@
//go:build windows //go:build windows
// +build windows
package winio package winio
import ( import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"errors"
"fmt" "fmt"
"strings" "strings"
"unicode/utf16" "unicode/utf16"
@@ -15,9 +15,6 @@ import (
const ( const (
reparseTagMountPoint = 0xA0000003 reparseTagMountPoint = 0xA0000003
reparseTagSymlink = 0xA000000C reparseTagSymlink = 0xA000000C
reparseTagLxSymlink = 0xA000001D // WSL/MSYS2 native symlinks
lxSymlinkVersion = 2 // LX symlink format version
) )
type reparseDataBuffer struct { type reparseDataBuffer struct {
@@ -34,7 +31,6 @@ type reparseDataBuffer struct {
type ReparsePoint struct { type ReparsePoint struct {
Target string Target string
IsMountPoint bool IsMountPoint bool
IsLxSymlink bool // True if this is an LX symlink (WSL/MSYS2 native)
} }
// UnsupportedReparsePointError is returned when trying to decode a non-symlink or // UnsupportedReparsePointError is returned when trying to decode a non-symlink or
@@ -55,19 +51,14 @@ func DecodeReparsePoint(b []byte) (*ReparsePoint, error) {
} }
func DecodeReparsePointData(tag uint32, b []byte) (*ReparsePoint, error) { func DecodeReparsePointData(tag uint32, b []byte) (*ReparsePoint, error) {
isMountPoint := false
switch tag { switch tag {
case reparseTagMountPoint: case reparseTagMountPoint:
return decodeWindowsReparsePointData(b, true) isMountPoint = true
case reparseTagSymlink: case reparseTagSymlink:
return decodeWindowsReparsePointData(b, false)
case reparseTagLxSymlink:
return decodeLxReparsePointData(b)
default: default:
return nil, &UnsupportedReparsePointError{tag} return nil, &UnsupportedReparsePointError{tag}
} }
}
func decodeWindowsReparsePointData(b []byte, isMountPoint bool) (*ReparsePoint, error) {
nameOffset := 8 + binary.LittleEndian.Uint16(b[4:6]) nameOffset := 8 + binary.LittleEndian.Uint16(b[4:6])
if !isMountPoint { if !isMountPoint {
nameOffset += 4 nameOffset += 4
@@ -78,56 +69,16 @@ func decodeWindowsReparsePointData(b []byte, isMountPoint bool) (*ReparsePoint,
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &ReparsePoint{Target: string(utf16.Decode(name)), IsMountPoint: isMountPoint, IsLxSymlink: false}, nil return &ReparsePoint{string(utf16.Decode(name)), isMountPoint}, nil
}
func decodeLxReparsePointData(b []byte) (*ReparsePoint, error) {
// LX symlinks store the target as UTF-8 after a 4-byte version field
if len(b) < 4 {
return nil, errors.New("LX symlink buffer too short")
}
targetBytes := b[4:]
for i, c := range targetBytes {
if c == 0 {
targetBytes = targetBytes[:i]
break
}
}
target := string(targetBytes)
return &ReparsePoint{Target: target, IsMountPoint: false, IsLxSymlink: true}, nil
} }
func isDriveLetter(c byte) bool { func isDriveLetter(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
} }
// EncodeReparsePoint encodes a Win32 REPARSE_DATA_BUFFER structure describing a symlink, // EncodeReparsePoint encodes a Win32 REPARSE_DATA_BUFFER structure describing a symlink or
// mount point, or LX symlink. // mount point.
func EncodeReparsePoint(rp *ReparsePoint) []byte { func EncodeReparsePoint(rp *ReparsePoint) []byte {
if rp == nil {
return nil
}
if rp.IsLxSymlink {
return encodeLxReparsePoint(rp)
}
return encodeWindowsReparsePoint(rp)
}
func encodeLxReparsePoint(rp *ReparsePoint) []byte {
// LX symlink: 4-byte version + UTF-8 target
targetBytes := []byte(rp.Target)
dataLength := 4 + len(targetBytes)
var b bytes.Buffer
_ = binary.Write(&b, binary.LittleEndian, uint32(reparseTagLxSymlink))
_ = binary.Write(&b, binary.LittleEndian, uint16(dataLength))
_ = binary.Write(&b, binary.LittleEndian, uint16(0))
_ = binary.Write(&b, binary.LittleEndian, uint32(lxSymlinkVersion))
_, _ = b.Write(targetBytes)
return b.Bytes()
}
func encodeWindowsReparsePoint(rp *ReparsePoint) []byte {
// Generate an NT path and determine if this is a relative path. // Generate an NT path and determine if this is a relative path.
var ntTarget string var ntTarget string
relative := false relative := false
+1
View File
@@ -1,4 +1,5 @@
//go:build windows //go:build windows
// +build windows
package winio package winio
+6 -2
View File
@@ -15,8 +15,12 @@ var _ unsafe.Pointer
// Do the interface allocations only once for common // Do the interface allocations only once for common
// Errno values. // Errno values.
const (
errnoERROR_IO_PENDING = 997
)
var ( var (
errERROR_IO_PENDING error = windows.ERROR_IO_PENDING errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
errERROR_EINVAL error = syscall.EINVAL errERROR_EINVAL error = syscall.EINVAL
) )
@@ -26,7 +30,7 @@ func errnoErr(e syscall.Errno) error {
switch e { switch e {
case 0: case 0:
return errERROR_EINVAL return errERROR_EINVAL
case windows.ERROR_IO_PENDING: case errnoERROR_IO_PENDING:
return errERROR_IO_PENDING return errERROR_IO_PENDING
} }
return e return e
-1
View File
@@ -1 +0,0 @@
* text=auto eol=lf
+15 -35
View File
@@ -1,50 +1,30 @@
version: "2"
linters: linters:
enable: enable:
- copyloopvar - exportloopref # Checks for pointers to enclosing loop variables
- dupword - gofmt
- goimports
- gosec - gosec
- govet
- ineffassign - ineffassign
- misspell - misspell
- nolintlint - nolintlint
- revive - revive
- staticcheck - staticcheck
- tenv # Detects using os.Setenv instead of t.Setenv since Go 1.17
- unconvert - unconvert
- unused - unused
- vet
- dupword # Checks for duplicate words in the source code
disable: disable:
- errcheck - errcheck
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- api
- cluster
- design
- docs
- docs/man
- releases
- reports
- test
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
paths:
- api
- cluster
- design
- docs
- docs/man
- releases
- reports
- test
run: run:
timeout: 5m timeout: 5m
skip-dirs:
- api
- cluster
- design
- docs
- docs/man
- releases
- reports
- test # e2e scripts
+9 -61
View File
@@ -40,11 +40,8 @@ package log
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"log/slog"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
lslog "github.com/sirupsen/logrus/hooks/slog"
) )
// G is a shorthand for [GetLogger]. // G is a shorthand for [GetLogger].
@@ -111,17 +108,10 @@ const (
PanicLevel Level = logrus.PanicLevel PanicLevel Level = logrus.PanicLevel
) )
// levelValue is a log level accepted by [SetLevel]. // SetLevel sets log level globally. It returns an error if the given
type levelValue interface { // level is not supported.
string | Level | slog.Level | lslog.Level | lslog.SlogLevel
}
// SetLevel sets the global log level.
// //
// The level may be specified as a string, a [Level], a [slog.Level], an // level can be one of:
// [lslog.Level], or an [lslog.SlogLevel].
//
// String levels are parsed using [logrus.ParseLevel] and may be one of:
// //
// - "trace" ([TraceLevel]) // - "trace" ([TraceLevel])
// - "debug" ([DebugLevel]) // - "debug" ([DebugLevel])
@@ -130,34 +120,10 @@ type levelValue interface {
// - "error" ([ErrorLevel]) // - "error" ([ErrorLevel])
// - "fatal" ([FatalLevel]) // - "fatal" ([FatalLevel])
// - "panic" ([PanicLevel]) // - "panic" ([PanicLevel])
// func SetLevel(level string) error {
// slog levels are mapped to Logrus levels using the mapping defined by lvl, err := logrus.ParseLevel(level)
// [lslog.SlogLevel]. [lslog.Level] values are accepted directly as their if err != nil {
// underlying Logrus level. return err
//
// SetLevel returns an error if a string level is not supported.
func SetLevel[T levelValue](level T) error {
var lvl Level
switch l := any(level).(type) {
case string:
var err error
lvl, err = logrus.ParseLevel(l)
if err != nil {
return err
}
case Level:
lvl = l
case slog.Level:
lvl = lslog.SlogLevel(l).Level()
case lslog.Level:
lvl = Level(l)
case lslog.SlogLevel:
lvl = l.Level()
} }
L.Logger.SetLevel(lvl) L.Logger.SetLevel(lvl)
@@ -183,39 +149,21 @@ const (
// SetFormat sets the log output format ([TextFormat] or [JSONFormat]). // SetFormat sets the log output format ([TextFormat] or [JSONFormat]).
func SetFormat(format OutputFormat) error { func SetFormat(format OutputFormat) error {
if slogOut != nil {
var handler slog.Handler
switch format {
case TextFormat:
handler = slog.NewTextHandler(slogOut, &slog.HandlerOptions{Level: loggerLevel{}})
case JSONFormat:
handler = slog.NewJSONHandler(slogOut, &slog.HandlerOptions{Level: loggerLevel{}})
default:
return fmt.Errorf("unknown log format: %s", format)
}
slog.SetDefault(slog.New(handler))
// Keep logrus formatting and output disabled, as both are handled by slog.
L.Logger.SetFormatter(discardFormatter{})
L.Logger.SetOutput(io.Discard)
return nil
}
switch format { switch format {
case TextFormat: case TextFormat:
L.Logger.SetFormatter(&logrus.TextFormatter{ L.Logger.SetFormatter(&logrus.TextFormatter{
TimestampFormat: RFC3339NanoFixed, TimestampFormat: RFC3339NanoFixed,
FullTimestamp: true, FullTimestamp: true,
}) })
return nil
case JSONFormat: case JSONFormat:
L.Logger.SetFormatter(&logrus.JSONFormatter{ L.Logger.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: RFC3339NanoFixed, TimestampFormat: RFC3339NanoFixed,
}) })
return nil
default: default:
return fmt.Errorf("unknown log format: %s", format) return fmt.Errorf("unknown log format: %s", format)
} }
return nil
} }
// WithLogger returns a new context with the provided logger. Use in // WithLogger returns a new context with the provided logger. Use in
-66
View File
@@ -1,66 +0,0 @@
/*
Copyright The containerd Authors.
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
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package log
import (
"io"
"log/slog"
"sync"
"github.com/sirupsen/logrus"
lslog "github.com/sirupsen/logrus/hooks/slog"
)
// slogOut is used to set the slog logger when setting output format.
var slogOut io.Writer
// slogOnce guards UseSlog so repeated calls do not stack up hooks or
// reset slogOut to the discard writer installed on the first call.
var slogOnce sync.Once
func UseSlog() {
slogOnce.Do(func() {
L.Logger.SetNoLock()
L.Logger.AddHook(slogHook{})
slogOut = L.Logger.Out
// Disable logrus formatting and output, as both are handled by slog.
L.Logger.SetFormatter(discardFormatter{})
L.Logger.SetOutput(io.Discard)
})
}
type slogHook struct{}
func (slogHook) Levels() []logrus.Level {
return logrus.AllLevels
}
func (slogHook) Fire(entry *logrus.Entry) error {
return lslog.NewHook(slog.Default(), nil).Fire(entry)
}
// loggerLevel exposes the current Logrus logger level as a slog.Leveler.
type loggerLevel struct{}
func (loggerLevel) Level() slog.Level {
return lslog.Level(L.Logger.GetLevel()).Level()
}
type discardFormatter struct{}
func (discardFormatter) Format(*logrus.Entry) ([]byte, error) { return nil, nil }
+1 -5
View File
@@ -9,17 +9,13 @@ import (
// NewTCPSocket creates a TCP socket listener with the specified address and // NewTCPSocket creates a TCP socket listener with the specified address and
// the specified tls configuration. If TLSConfig is set, will encapsulate the // the specified tls configuration. If TLSConfig is set, will encapsulate the
// TCP listener inside a TLS one. // TCP listener inside a TLS one.
// If no ALPN protocols are configured, it defaults to HTTP/1.1.
func NewTCPSocket(addr string, tlsConfig *tls.Config) (net.Listener, error) { func NewTCPSocket(addr string, tlsConfig *tls.Config) (net.Listener, error) {
l, err := net.Listen("tcp", addr) l, err := net.Listen("tcp", addr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if tlsConfig != nil { if tlsConfig != nil {
if len(tlsConfig.NextProtos) == 0 { tlsConfig.NextProtos = []string{"http/1.1"}
tlsConfig = tlsConfig.Clone()
tlsConfig.NextProtos = []string{"http/1.1"}
}
l = tls.NewListener(l, tlsConfig) l = tls.NewListener(l, tlsConfig)
} }
return l, nil return l, nil
+3 -33
View File
@@ -408,10 +408,7 @@ func (ctx *genericEncrypter) EncryptWithAuthData(plaintext, aad []byte) (*JSONWe
(*obj.protected)[k] = makeRawMessage(b) (*obj.protected)[k] = makeRawMessage(b)
} }
authData, err := obj.computeAuthData() authData := obj.computeAuthData()
if err != nil {
return nil, err
}
parts, err := ctx.cipher.encrypt(cek, authData, plaintext) parts, err := ctx.cipher.encrypt(cek, authData, plaintext)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -431,13 +428,6 @@ func (ctx *genericEncrypter) Options() EncrypterOptions {
} }
} }
func validateContentEncryptionKeySize(cek []byte, cipher contentCipher) error {
if len(cek) != cipher.keySize() {
return ErrInvalidKeySize
}
return nil
}
// Decrypt and validate the object and return the plaintext. This // Decrypt and validate the object and return the plaintext. This
// function does not support multi-recipient. If you desire multi-recipient // function does not support multi-recipient. If you desire multi-recipient
// decryption use DecryptMulti instead. // decryption use DecryptMulti instead.
@@ -495,10 +485,7 @@ func (obj JSONWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error)
tag: obj.tag, tag: obj.tag,
} }
authData, err := obj.computeAuthData() authData := obj.computeAuthData()
if err != nil {
return nil, err
}
var plaintext []byte var plaintext []byte
recipient := obj.recipients[0] recipient := obj.recipients[0]
@@ -508,10 +495,6 @@ func (obj JSONWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error)
if err != nil { if err != nil {
return nil, ErrCryptoFailure return nil, ErrCryptoFailure
} }
err = validateContentEncryptionKeySize(cek, cipher)
if err != nil {
return nil, ErrCryptoFailure
}
// Found a valid CEK -- let's try to decrypt. // Found a valid CEK -- let's try to decrypt.
plaintext, err = cipher.decrypt(cek, authData, parts) plaintext, err = cipher.decrypt(cek, authData, parts)
@@ -537,12 +520,6 @@ func (obj JSONWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error)
// //
// Automatically decompresses plaintext, but returns an error if the decompressed // Automatically decompresses plaintext, but returns an error if the decompressed
// data would be >250kB or >3x the size of the compressed data, whichever is larger. // data would be >250kB or >3x the size of the compressed data, whichever is larger.
//
// Calling DecryptMulti() with PBES2 enabled can result in excessive CPU work.
// The PBES2 key unwrapping algorithms are intentionally expensive, and have to
// be run for each recipient. To be safe: don't allow PBES2 algorithms in calls
// to ParseEncrypted / ParseEncryptedJSON. If your application requires PBES2,
// prefer Decrypt(), which will only process single-recipient messages.
func (obj JSONWebEncryption) DecryptMulti(decryptionKey interface{}) (int, Header, []byte, error) { func (obj JSONWebEncryption) DecryptMulti(decryptionKey interface{}) (int, Header, []byte, error) {
globalHeaders := obj.mergedHeaders(nil) globalHeaders := obj.mergedHeaders(nil)
@@ -576,10 +553,7 @@ func (obj JSONWebEncryption) DecryptMulti(decryptionKey interface{}) (int, Heade
tag: obj.tag, tag: obj.tag,
} }
authData, err := obj.computeAuthData() authData := obj.computeAuthData()
if err != nil {
return -1, Header{}, nil, err
}
index := -1 index := -1
var plaintext []byte var plaintext []byte
@@ -598,10 +572,6 @@ func (obj JSONWebEncryption) DecryptMulti(decryptionKey interface{}) (int, Heade
if err != nil { if err != nil {
continue continue
} }
err = validateContentEncryptionKeySize(cek, cipher)
if err != nil {
continue
}
// Found a valid CEK -- let's try to decrypt. // Found a valid CEK -- let's try to decrypt.
plaintext, err = cipher.decrypt(cek, authData, parts) plaintext, err = cipher.decrypt(cek, authData, parts)
+32 -6
View File
@@ -30,6 +30,30 @@ import (
"github.com/go-jose/go-jose/v4/json" "github.com/go-jose/go-jose/v4/json"
) )
// Helper function to serialize known-good objects.
// Precondition: value is not a nil pointer.
func mustSerializeJSON(value interface{}) []byte {
out, err := json.Marshal(value)
if err != nil {
panic(err)
}
// We never want to serialize the top-level value "null," since it's not a
// valid JOSE message. But if a caller passes in a nil pointer to this method,
// MarshalJSON will happily serialize it as the top-level value "null". If
// that value is then embedded in another operation, for instance by being
// base64-encoded and fed as input to a signing algorithm
// (https://github.com/go-jose/go-jose/issues/22), the result will be
// incorrect. Because this method is intended for known-good objects, and a nil
// pointer is not a known-good object, we are free to panic in this case.
// Note: It's not possible to directly check whether the data pointed at by an
// interface is a nil pointer, so we do this hacky workaround.
// https://groups.google.com/forum/#!topic/golang-nuts/wnH302gBa4I
if string(out) == "null" {
panic("Tried to serialize a nil pointer.")
}
return out
}
// Strip all newlines and whitespace // Strip all newlines and whitespace
func stripWhitespace(data string) string { func stripWhitespace(data string) string {
buf := strings.Builder{} buf := strings.Builder{}
@@ -98,8 +122,6 @@ func inflate(input []byte) ([]byte, error) {
} }
// byteBuffer represents a slice of bytes that can be serialized to url-safe base64. // byteBuffer represents a slice of bytes that can be serialized to url-safe base64.
//
// A nil byteBuffer represents an empty slice of bytes.
type byteBuffer struct { type byteBuffer struct {
data []byte data []byte
} }
@@ -147,13 +169,13 @@ func (b *byteBuffer) UnmarshalJSON(data []byte) error {
return err return err
} }
b.data = decoded *b = *newBuffer(decoded)
return nil return nil
} }
func (b *byteBuffer) base64() string { func (b *byteBuffer) base64() string {
return base64.RawURLEncoding.EncodeToString(b.bytes()) return base64.RawURLEncoding.EncodeToString(b.data)
} }
func (b *byteBuffer) bytes() []byte { func (b *byteBuffer) bytes() []byte {
@@ -164,8 +186,12 @@ func (b *byteBuffer) bytes() []byte {
return b.data return b.data
} }
func (b *byteBuffer) bigInt() *big.Int { func (b byteBuffer) bigInt() *big.Int {
return new(big.Int).SetBytes(b.bytes()) return new(big.Int).SetBytes(b.data)
}
func (b byteBuffer) toInt() int {
return int(b.bigInt().Int64())
} }
func base64EncodeLen(sl []byte) int { func base64EncodeLen(sl []byte) int {
-13
View File
@@ -671,19 +671,6 @@ func (d *decodeState) object(v reflect.Value) {
for _, i := range f.index { for _, i := range f.index {
if subv.Kind() == reflect.Ptr { if subv.Kind() == reflect.Ptr {
if subv.IsNil() { if subv.IsNil() {
// If a struct embeds a pointer to an unexported type,
// it is not possible to set a newly allocated value
// since the field is unexported.
//
// See https://golang.org/issue/21357
if !subv.CanSet() {
d.saveError(fmt.Errorf("json: cannot set embedded pointer to unexported struct: %v", subv.Type().Elem()))
// Invalidate subv to ensure d.value(subv) skips over
// the JSON value without assigning it to subv.
subv = reflect.Value{}
destring = false
break
}
subv.Set(reflect.New(subv.Type().Elem())) subv.Set(reflect.New(subv.Type().Elem()))
} }
subv = subv.Elem() subv = subv.Elem()
+1 -13
View File
@@ -1025,19 +1025,7 @@ func typeFields(t reflect.Type) []field {
// Scan f.typ for fields to include. // Scan f.typ for fields to include.
for i := 0; i < f.typ.NumField(); i++ { for i := 0; i < f.typ.NumField(); i++ {
sf := f.typ.Field(i) sf := f.typ.Field(i)
if sf.Anonymous { if sf.PkgPath != "" && !sf.Anonymous { // unexported
t := sf.Type
if t.Kind() == reflect.Pointer {
t = t.Elem()
}
if !sf.IsExported() && t.Kind() != reflect.Struct {
// Ignore embedded fields of unexported non-struct types.
continue
}
// Do not ignore embedded fields of unexported struct types
// since they may have exported fields.
} else if !sf.IsExported() {
// Ignore unexported non-embedded fields.
continue continue
} }
tag := sf.Tag.Get("json") tag := sf.Tag.Get("json")
+25 -59
View File
@@ -25,12 +25,6 @@ import (
"github.com/go-jose/go-jose/v4/json" "github.com/go-jose/go-jose/v4/json"
) )
// MaxRecipients is the maximum number of recipients of a JWE at parse time.
// Higher values mean more potential work done across untrusted input.
//
// If modifying MaxRecipients, do it before any parsing occurs.
var MaxRecipients = 32
// rawJSONWebEncryption represents a raw JWE JSON object. Used for parsing/serializing. // rawJSONWebEncryption represents a raw JWE JSON object. Used for parsing/serializing.
type rawJSONWebEncryption struct { type rawJSONWebEncryption struct {
Protected *byteBuffer `json:"protected,omitempty"` Protected *byteBuffer `json:"protected,omitempty"`
@@ -90,18 +84,14 @@ func (obj JSONWebEncryption) mergedHeaders(recipient *recipientInfo) rawHeader {
} }
// Get the additional authenticated data from a JWE object. // Get the additional authenticated data from a JWE object.
func (obj JSONWebEncryption) computeAuthData() ([]byte, error) { func (obj JSONWebEncryption) computeAuthData() []byte {
var protected string var protected string
switch { switch {
case obj.original != nil && obj.original.Protected != nil: case obj.original != nil && obj.original.Protected != nil:
protected = obj.original.Protected.base64() protected = obj.original.Protected.base64()
case obj.protected != nil: case obj.protected != nil:
serializedProtected, err := json.Marshal(obj.protected) protected = base64.RawURLEncoding.EncodeToString(mustSerializeJSON((obj.protected)))
if err != nil {
return nil, err
}
protected = base64.RawURLEncoding.EncodeToString(serializedProtected)
default: default:
protected = "" protected = ""
} }
@@ -112,7 +102,7 @@ func (obj JSONWebEncryption) computeAuthData() ([]byte, error) {
output = append(output, []byte(base64.RawURLEncoding.EncodeToString(obj.aad))...) output = append(output, []byte(base64.RawURLEncoding.EncodeToString(obj.aad))...)
} }
return output, nil return output
} }
func containsKeyAlgorithm(haystack []KeyAlgorithm, needle KeyAlgorithm) bool { func containsKeyAlgorithm(haystack []KeyAlgorithm, needle KeyAlgorithm) bool {
@@ -138,10 +128,11 @@ func containsContentEncryption(haystack []ContentEncryption, needle ContentEncry
// https://datatracker.ietf.org/doc/html/rfc7516#section-3.1 // https://datatracker.ietf.org/doc/html/rfc7516#section-3.1
// https://datatracker.ietf.org/doc/html/rfc7516#section-3.2 // https://datatracker.ietf.org/doc/html/rfc7516#section-3.2
// //
// The keyAlgorithms and contentEncryption parameters contain the allowable algorithms in the "alg" and "enc" // The keyAlgorithms and contentEncryption parameters are used to validate the "alg" and "enc"
// header parameters respectively. They must be non-empty. Non-matching algorithms cause a parse error. // header parameters respectively. They must be nonempty, and each "alg" or "enc" header in
// // parsed data must contain a value that is present in the corresponding parameter. That
// Deprecated: use ParseEncryptedJSON or ParseEncryptedCompact depending on which serialization you expect. // includes the protected and unprotected headers as well as all recipients. To accept
// multiple algorithms, pass a slice of all the algorithms you want to accept.
func ParseEncrypted(input string, func ParseEncrypted(input string,
keyEncryptionAlgorithms []KeyAlgorithm, keyEncryptionAlgorithms []KeyAlgorithm,
contentEncryption []ContentEncryption, contentEncryption []ContentEncryption,
@@ -154,13 +145,9 @@ func ParseEncrypted(input string,
return ParseEncryptedCompact(input, keyEncryptionAlgorithms, contentEncryption) return ParseEncryptedCompact(input, keyEncryptionAlgorithms, contentEncryption)
} }
// ParseEncryptedJSON parses a message in JWE JSON Serialization. Messages with // ParseEncryptedJSON parses a message in JWE JSON Serialization.
// more than [MaxRecipients] recipients are rejected.
// //
// https://datatracker.ietf.org/doc/html/rfc7516#section-3.2 // https://datatracker.ietf.org/doc/html/rfc7516#section-3.2
//
// The keyAlgorithms and contentEncryption parameters contain the allowable algorithms in the "alg" and "enc"
// header parameters respectively. They must be non-empty. Any non-allowed algorithms will cause parsing to fail.
func ParseEncryptedJSON( func ParseEncryptedJSON(
input string, input string,
keyEncryptionAlgorithms []KeyAlgorithm, keyEncryptionAlgorithms []KeyAlgorithm,
@@ -190,9 +177,6 @@ func (parsed *rawJSONWebEncryption) sanitized(
if len(contentEncryption) == 0 { if len(contentEncryption) == 0 {
return nil, errors.New("go-jose/go-jose: no content encryption algorithms provided") return nil, errors.New("go-jose/go-jose: no content encryption algorithms provided")
} }
if len(parsed.Recipients) > MaxRecipients {
return nil, fmt.Errorf("go-jose/go-jose: too many recipients in JWE; maximum is %d", MaxRecipients)
}
obj := &JSONWebEncryption{ obj := &JSONWebEncryption{
original: parsed, original: parsed,
@@ -211,11 +195,21 @@ func (parsed *rawJSONWebEncryption) sanitized(
} }
} }
protected, err := parseProtectedHeader(parsed.Protected) if parsed.Protected != nil && len(parsed.Protected.bytes()) > 0 {
err := json.Unmarshal(parsed.Protected.bytes(), &obj.protected)
if err != nil {
return nil, fmt.Errorf("go-jose/go-jose: invalid protected header: %s, %s", err, parsed.Protected.base64())
}
}
// Note: this must be called _after_ we parse the protected header,
// otherwise fields from the protected header will not get picked up.
var err error
mergedHeaders := obj.mergedHeaders(nil)
obj.Header, err = mergedHeaders.sanitized()
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("go-jose/go-jose: cannot sanitize merged headers: %v (%v)", err, mergedHeaders)
} }
obj.protected = protected
if len(parsed.Recipients) == 0 { if len(parsed.Recipients) == 0 {
obj.recipients = []recipientInfo{ obj.recipients = []recipientInfo{
@@ -270,14 +264,6 @@ func (parsed *rawJSONWebEncryption) sanitized(
} }
} }
// Note: this must be called _after_ we parse the protected header,
// otherwise fields from the protected header will not get picked up.
mergedHeaders := obj.mergedHeaders(nil)
obj.Header, err = mergedHeaders.sanitized()
if err != nil {
return nil, fmt.Errorf("go-jose/go-jose: cannot sanitize merged headers: %w", err)
}
obj.iv = parsed.Iv.bytes() obj.iv = parsed.Iv.bytes()
obj.ciphertext = parsed.Ciphertext.bytes() obj.ciphertext = parsed.Ciphertext.bytes()
obj.tag = parsed.Tag.bytes() obj.tag = parsed.Tag.bytes()
@@ -301,9 +287,6 @@ func validateAlgEnc(headers rawHeader, keyAlgorithms []KeyAlgorithm, contentEncr
// ParseEncryptedCompact parses a message in JWE Compact Serialization. // ParseEncryptedCompact parses a message in JWE Compact Serialization.
// //
// https://datatracker.ietf.org/doc/html/rfc7516#section-3.1 // https://datatracker.ietf.org/doc/html/rfc7516#section-3.1
//
// The keyAlgorithms and contentEncryption parameters contain the allowable algorithms in the "alg" and "enc"
// header parameters respectively. They must be non-empty. Any non-allowed algorithms will cause parsing to fail.
func ParseEncryptedCompact( func ParseEncryptedCompact(
input string, input string,
keyAlgorithms []KeyAlgorithm, keyAlgorithms []KeyAlgorithm,
@@ -371,10 +354,7 @@ func (obj JSONWebEncryption) CompactSerialize() (string, error) {
return "", ErrNotSupported return "", ErrNotSupported
} }
serializedProtected, err := json.Marshal(obj.protected) serializedProtected := mustSerializeJSON(obj.protected)
if err != nil {
return "", err
}
return base64JoinWithDots( return base64JoinWithDots(
serializedProtected, serializedProtected,
@@ -386,13 +366,7 @@ func (obj JSONWebEncryption) CompactSerialize() (string, error) {
} }
// FullSerialize serializes an object using the full JSON serialization format. // FullSerialize serializes an object using the full JSON serialization format.
//
// It returns "" if obj cannot be serialized, for example if it has no recipients.
func (obj JSONWebEncryption) FullSerialize() string { func (obj JSONWebEncryption) FullSerialize() string {
if len(obj.recipients) == 0 {
return ""
}
raw := rawJSONWebEncryption{ raw := rawJSONWebEncryption{
Unprotected: obj.unprotected, Unprotected: obj.unprotected,
Iv: newBuffer(obj.iv), Iv: newBuffer(obj.iv),
@@ -418,16 +392,8 @@ func (obj JSONWebEncryption) FullSerialize() string {
} }
if obj.protected != nil { if obj.protected != nil {
serializedProtected, err := json.Marshal(obj.protected) raw.Protected = newBuffer(mustSerializeJSON(obj.protected))
if err != nil {
return ""
}
raw.Protected = newBuffer(serializedProtected)
} }
out, err := json.Marshal(raw) return string(mustSerializeJSON(raw))
if err != nil {
return ""
}
return string(out)
} }
+24 -74
View File
@@ -30,7 +30,6 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"math"
"math/big" "math/big"
"net/url" "net/url"
"reflect" "reflect"
@@ -178,29 +177,13 @@ func (k JSONWebKey) MarshalJSON() ([]byte, error) {
// UnmarshalJSON reads a key from its JSON representation. // UnmarshalJSON reads a key from its JSON representation.
// //
// Returns ErrUnsupportedKeyType for unrecognized or unsupported "kty" header values. // Returns ErrUnsupportedKeyType for unrecognized or unsupported "kty" header values.
// func (k *JSONWebKey) UnmarshalJSON(data []byte) (err error) {
// Directly unmarshaling a JWK is not safe for untrusted input.
func (k *JSONWebKey) UnmarshalJSON(data []byte) error {
return k.unmarshalJSON(data, true)
}
// unmarshalJSON reads a key from its JSON representation.
//
// If allowPrivate is false, a JWK with private key material is rejected and the
// function is safe for use against untrusted input.
func (k *JSONWebKey) unmarshalJSON(data []byte, allowPrivate bool) (err error) {
var raw rawJSONWebKey var raw rawJSONWebKey
err = json.Unmarshal(data, &raw) err = json.Unmarshal(data, &raw)
if err != nil { if err != nil {
return err return err
} }
if !allowPrivate &&
(raw.K != nil || raw.D != nil || raw.P != nil || raw.Q != nil ||
raw.Dp != nil || raw.Dq != nil || raw.Qi != nil) {
return errors.New("go-jose/go-jose: JWK contains private parameters")
}
certs, err := parseCertificateChain(raw.X5c) certs, err := parseCertificateChain(raw.X5c)
if err != nil { if err != nil {
return fmt.Errorf("go-jose/go-jose: failed to unmarshal x5c field: %s", err) return fmt.Errorf("go-jose/go-jose: failed to unmarshal x5c field: %s", err)
@@ -250,7 +233,7 @@ func (k *JSONWebKey) unmarshalJSON(data []byte, allowPrivate bool) (err error) {
keyPub = key keyPub = key
} }
case "oct": case "oct":
if len(certs) > 0 { if certPub != nil {
return errors.New("go-jose/go-jose: invalid JWK, found 'oct' (symmetric) key with cert chain") return errors.New("go-jose/go-jose: invalid JWK, found 'oct' (symmetric) key with cert chain")
} }
key, err = raw.symmetricKey() key, err = raw.symmetricKey()
@@ -289,8 +272,10 @@ func (k *JSONWebKey) unmarshalJSON(data []byte, allowPrivate bool) (err error) {
return ErrUnsupportedKeyType return ErrUnsupportedKeyType
} }
if len(certs) > 0 && (certPub == nil || !reflect.DeepEqual(certPub, keyPub)) { if certPub != nil && keyPub != nil {
return errors.New("go-jose/go-jose: invalid JWK, public keys in key and x5c fields do not match") if !reflect.DeepEqual(certPub, keyPub) {
return errors.New("go-jose/go-jose: invalid JWK, public keys in key and x5c fields do not match")
}
} }
*k = JSONWebKey{Key: key, KeyID: raw.Kid, Algorithm: raw.Alg, Use: raw.Use, Certificates: certs} *k = JSONWebKey{Key: key, KeyID: raw.Kid, Algorithm: raw.Alg, Use: raw.Use, Certificates: certs}
@@ -407,9 +392,6 @@ func ecThumbprintInput(curve elliptic.Curve, x, y *big.Int) (string, error) {
} }
func rsaThumbprintInput(n *big.Int, e int) (string, error) { func rsaThumbprintInput(n *big.Int, e int) (string, error) {
if n == nil || e <= 0 {
return "", errors.New("go-jose/go-jose: invalid RSA key")
}
return fmt.Sprintf(rsaThumbprintTemplate, return fmt.Sprintf(rsaThumbprintTemplate,
newBufferFromInt(uint64(e)).base64(), newBufferFromInt(uint64(e)).base64(),
newBuffer(n.Bytes()).base64()), nil newBuffer(n.Bytes()).base64()), nil
@@ -523,41 +505,14 @@ func (k *JSONWebKey) Valid() bool {
return true return true
} }
// MaxRSAModulusBits is the largest RSA modulus size, in bits, that will be
// accepted when parsing a JWK or verifying an x5c certificate chain with
// [Header.Certificates]. Applications that need larger keys can raise this
// limit.
//
// If modifying MaxRSAModulusBits, do it before any parsing occurs.
var MaxRSAModulusBits = 4096
func (key rawJSONWebKey) rsaPublicKey() (*rsa.PublicKey, error) { func (key rawJSONWebKey) rsaPublicKey() (*rsa.PublicKey, error) {
return rsaPublicKey(key.N, key.E) if key.N == nil || key.E == nil {
}
func rsaPublicKey(N, E *byteBuffer) (*rsa.PublicKey, error) {
if N == nil || E == nil {
return nil, fmt.Errorf("go-jose/go-jose: invalid RSA key, missing n/e values") return nil, fmt.Errorf("go-jose/go-jose: invalid RSA key, missing n/e values")
} }
n := N.bigInt()
if n.BitLen() > MaxRSAModulusBits {
return nil, fmt.Errorf("go-jose/go-jose: invalid RSA key, modulus too large")
}
e := new(big.Int).SetBytes(E.bytes())
if !e.IsInt64() {
return nil, fmt.Errorf("go-jose/go-jose: invalid RSA key, bad e value")
}
eInt64 := e.Int64()
if eInt64 < 2 || eInt64 > math.MaxInt {
return nil, fmt.Errorf("go-jose/go-jose: invalid RSA key, bad e value")
}
return &rsa.PublicKey{ return &rsa.PublicKey{
N: n, N: key.N.bigInt(),
E: int(eInt64), E: key.E.toInt(),
}, nil }, nil
} }
@@ -601,11 +556,11 @@ func (key rawJSONWebKey) ecPublicKey() (*ecdsa.PublicKey, error) {
// The length of this octet string MUST be the full size of a coordinate for // The length of this octet string MUST be the full size of a coordinate for
// the curve specified in the "crv" parameter. // the curve specified in the "crv" parameter.
// https://tools.ietf.org/html/rfc7518#section-6.2.1.2 // https://tools.ietf.org/html/rfc7518#section-6.2.1.2
if curveSize(curve) != len(key.X.bytes()) { if curveSize(curve) != len(key.X.data) {
return nil, fmt.Errorf("go-jose/go-jose: invalid EC public key, wrong length for x") return nil, fmt.Errorf("go-jose/go-jose: invalid EC public key, wrong length for x")
} }
if curveSize(curve) != len(key.Y.bytes()) { if curveSize(curve) != len(key.Y.data) {
return nil, fmt.Errorf("go-jose/go-jose: invalid EC public key, wrong length for y") return nil, fmt.Errorf("go-jose/go-jose: invalid EC public key, wrong length for y")
} }
@@ -718,21 +673,16 @@ func (key rawJSONWebKey) rsaPrivateKey() (*rsa.PrivateKey, error) {
return nil, fmt.Errorf("go-jose/go-jose: invalid RSA private key, missing %s value(s)", strings.Join(missing, ", ")) return nil, fmt.Errorf("go-jose/go-jose: invalid RSA private key, missing %s value(s)", strings.Join(missing, ", "))
} }
pubKey, err := rsaPublicKey(key.N, key.E)
if err != nil {
return nil, err
}
n := pubKey.N
p, q := key.P.bigInt(), key.Q.bigInt()
if p.BitLen()+q.BitLen() > n.BitLen()+1 || new(big.Int).Mul(p, q).Cmp(n) != 0 {
return nil, errors.New("go-jose/go-jose: invalid RSA private key, p * q != n")
}
rv := &rsa.PrivateKey{ rv := &rsa.PrivateKey{
PublicKey: *pubKey, PublicKey: rsa.PublicKey{
D: key.D.bigInt(), N: key.N.bigInt(),
Primes: []*big.Int{p, q}, E: key.E.toInt(),
},
D: key.D.bigInt(),
Primes: []*big.Int{
key.P.bigInt(),
key.Q.bigInt(),
},
} }
if key.Dp != nil { if key.Dp != nil {
@@ -745,7 +695,7 @@ func (key rawJSONWebKey) rsaPrivateKey() (*rsa.PrivateKey, error) {
rv.Precomputed.Qinv = key.Qi.bigInt() rv.Precomputed.Qinv = key.Qi.bigInt()
} }
err = rv.Validate() err := rv.Validate()
return rv, err return rv, err
} }
@@ -819,16 +769,16 @@ func (key rawJSONWebKey) ecPrivateKey() (*ecdsa.PrivateKey, error) {
// The length of this octet string MUST be the full size of a coordinate for // The length of this octet string MUST be the full size of a coordinate for
// the curve specified in the "crv" parameter. // the curve specified in the "crv" parameter.
// https://tools.ietf.org/html/rfc7518#section-6.2.1.2 // https://tools.ietf.org/html/rfc7518#section-6.2.1.2
if curveSize(curve) != len(key.X.bytes()) { if curveSize(curve) != len(key.X.data) {
return nil, fmt.Errorf("go-jose/go-jose: invalid EC private key, wrong length for x") return nil, fmt.Errorf("go-jose/go-jose: invalid EC private key, wrong length for x")
} }
if curveSize(curve) != len(key.Y.bytes()) { if curveSize(curve) != len(key.Y.data) {
return nil, fmt.Errorf("go-jose/go-jose: invalid EC private key, wrong length for y") return nil, fmt.Errorf("go-jose/go-jose: invalid EC private key, wrong length for y")
} }
// https://tools.ietf.org/html/rfc7518#section-6.2.2.1 // https://tools.ietf.org/html/rfc7518#section-6.2.2.1
if dSize(curve) != len(key.D.bytes()) { if dSize(curve) != len(key.D.data) {
return nil, fmt.Errorf("go-jose/go-jose: invalid EC private key, wrong length for d") return nil, fmt.Errorf("go-jose/go-jose: invalid EC private key, wrong length for d")
} }
+33 -80
View File
@@ -27,12 +27,6 @@ import (
"github.com/go-jose/go-jose/v4/json" "github.com/go-jose/go-jose/v4/json"
) )
// MaxSignatures is the maximum number of signatures in a JWS at parse time.
// Higher values mean more potential work done across untrusted input.
//
// If modifying MaxSignatures, do it before any parsing occurs.
var MaxSignatures = 32
// rawJSONWebSignature represents a raw JWS JSON object. Used for parsing/serializing. // rawJSONWebSignature represents a raw JWS JSON object. Used for parsing/serializing.
type rawJSONWebSignature struct { type rawJSONWebSignature struct {
Payload *byteBuffer `json:"payload,omitempty"` Payload *byteBuffer `json:"payload,omitempty"`
@@ -92,11 +86,6 @@ type Signature struct {
// Verify() after parsing to validate the signature and obtain the payload. // Verify() after parsing to validate the signature and obtain the payload.
// //
// https://datatracker.ietf.org/doc/html/rfc7515#section-7 // https://datatracker.ietf.org/doc/html/rfc7515#section-7
//
// The signatureAlgorithms parameter contains the allowable algorithms in the "alg" header parameter.
// It must be non-empty. Non-matching algorithms cause a parse error.
//
// Deprecated: call ParseSignedJSON or ParseSignedCompact depending on which serialization you expect.
func ParseSigned( func ParseSigned(
signature string, signature string,
signatureAlgorithms []SignatureAlgorithm, signatureAlgorithms []SignatureAlgorithm,
@@ -119,9 +108,6 @@ func ParseSigned(
// Verify() after parsing to validate the signature and obtain the payload. // Verify() after parsing to validate the signature and obtain the payload.
// //
// https://datatracker.ietf.org/doc/html/rfc7515#section-7.1 // https://datatracker.ietf.org/doc/html/rfc7515#section-7.1
//
// The signatureAlgorithms parameter contains the allowable algorithms in the "alg" header parameter.
// It must be non-empty. Non-matching algorithms cause a parse error.
func ParseSignedCompact( func ParseSignedCompact(
signature string, signature string,
signatureAlgorithms []SignatureAlgorithm, signatureAlgorithms []SignatureAlgorithm,
@@ -139,9 +125,6 @@ func ParseSignedCompact(
// parsing. Call Verify() after parsing to validate the signature and obtain the payload. // parsing. Call Verify() after parsing to validate the signature and obtain the payload.
// //
// https://datatracker.ietf.org/doc/html/rfc7515#appendix-F // https://datatracker.ietf.org/doc/html/rfc7515#appendix-F
//
// The signatureAlgorithms parameter contains the allowable algorithms in the "alg" header parameter.
// It must be non-empty. Any non-allowed algorithms will cause parsing to fail.
func ParseDetached( func ParseDetached(
signature string, signature string,
payload []byte, payload []byte,
@@ -174,11 +157,7 @@ func (obj JSONWebSignature) computeAuthData(payload []byte, signature *Signature
authData.WriteString(signature.original.Protected.base64()) authData.WriteString(signature.original.Protected.base64())
} else if signature.protected != nil { } else if signature.protected != nil {
protectedHeader = signature.protected protectedHeader = signature.protected
serializedProtected, err := json.Marshal(protectedHeader) authData.WriteString(base64.RawURLEncoding.EncodeToString(mustSerializeJSON(protectedHeader)))
if err != nil {
return nil, err
}
authData.WriteString(base64.RawURLEncoding.EncodeToString(serializedProtected))
} }
needsBase64 := true needsBase64 := true
@@ -201,13 +180,9 @@ func (obj JSONWebSignature) computeAuthData(payload []byte, signature *Signature
return authData.Bytes(), nil return authData.Bytes(), nil
} }
// ParseSignedJSON parses a message in JWS JSON Serialization. Messages with // ParseSignedJSON parses a message in JWS JSON Serialization.
// more than [MaxSignatures] signatures are rejected.
// //
// https://datatracker.ietf.org/doc/html/rfc7515#section-7.2 // https://datatracker.ietf.org/doc/html/rfc7515#section-7.2
//
// The signatureAlgorithms parameter contains the allowable algorithms in the "alg" header parameter.
// It must be non-empty. Non-matching algorithms cause a parse error.
func ParseSignedJSON( func ParseSignedJSON(
input string, input string,
signatureAlgorithms []SignatureAlgorithm, signatureAlgorithms []SignatureAlgorithm,
@@ -261,9 +236,6 @@ func (parsed *rawJSONWebSignature) sanitized(signatureAlgorithms []SignatureAlgo
if parsed.Payload == nil { if parsed.Payload == nil {
return nil, fmt.Errorf("go-jose/go-jose: missing payload in JWS message") return nil, fmt.Errorf("go-jose/go-jose: missing payload in JWS message")
} }
if len(parsed.Signatures) > MaxSignatures {
return nil, fmt.Errorf("go-jose/go-jose: too many signatures in JWS; maximum is %d", MaxSignatures)
}
obj := &JSONWebSignature{ obj := &JSONWebSignature{
payload: parsed.Payload.bytes(), payload: parsed.Payload.bytes(),
@@ -273,13 +245,14 @@ func (parsed *rawJSONWebSignature) sanitized(signatureAlgorithms []SignatureAlgo
if len(parsed.Signatures) == 0 { if len(parsed.Signatures) == 0 {
// No signatures array, must be flattened serialization // No signatures array, must be flattened serialization
signature := Signature{} signature := Signature{}
prot, err := parseProtectedHeader(parsed.Protected) if parsed.Protected != nil && len(parsed.Protected.bytes()) > 0 {
if err != nil { signature.protected = &rawHeader{}
return nil, err err := json.Unmarshal(parsed.Protected.bytes(), signature.protected)
if err != nil {
return nil, err
}
} }
signature.protected = prot
// Check that there is not a nonce in the unprotected header // Check that there is not a nonce in the unprotected header
if parsed.Header != nil && parsed.Header.getNonce() != "" { if parsed.Header != nil && parsed.Header.getNonce() != "" {
return nil, ErrUnprotectedNonce return nil, ErrUnprotectedNonce
@@ -302,17 +275,17 @@ func (parsed *rawJSONWebSignature) sanitized(signatureAlgorithms []SignatureAlgo
Signature: parsed.Signature, Signature: parsed.Signature,
} }
mergedHeaders := signature.mergedHeaders() var err error
alg := mergedHeaders.getSignatureAlgorithm() signature.Header, err = signature.mergedHeaders().sanitized()
if !containsSignatureAlgorithm(signatureAlgorithms, alg) {
return nil, newErrUnexpectedSignatureAlgorithm(alg, signatureAlgorithms)
}
signature.Header, err = mergedHeaders.sanitized()
if err != nil { if err != nil {
return nil, err return nil, err
} }
alg := SignatureAlgorithm(signature.Header.Algorithm)
if !containsSignatureAlgorithm(signatureAlgorithms, alg) {
return nil, newErrUnexpectedSignatureAlgorithm(alg, signatureAlgorithms)
}
if signature.header != nil { if signature.header != nil {
signature.Unprotected, err = signature.header.sanitized() signature.Unprotected, err = signature.header.sanitized()
if err != nil { if err != nil {
@@ -337,31 +310,30 @@ func (parsed *rawJSONWebSignature) sanitized(signatureAlgorithms []SignatureAlgo
} }
for i, sig := range parsed.Signatures { for i, sig := range parsed.Signatures {
prot, err := parseProtectedHeader(sig.Protected) if sig.Protected != nil && len(sig.Protected.bytes()) > 0 {
if err != nil { obj.Signatures[i].protected = &rawHeader{}
return nil, err err := json.Unmarshal(sig.Protected.bytes(), obj.Signatures[i].protected)
if err != nil {
return nil, err
}
} }
obj.Signatures[i].protected = prot
// Check that there is not a nonce in the unprotected header // Check that there is not a nonce in the unprotected header
if sig.Header != nil && sig.Header.getNonce() != "" { if sig.Header != nil && sig.Header.getNonce() != "" {
return nil, ErrUnprotectedNonce return nil, ErrUnprotectedNonce
} }
obj.Signatures[i].header = sig.Header var err error
obj.Signatures[i].Header, err = obj.Signatures[i].mergedHeaders().sanitized()
mergedHeaders := obj.Signatures[i].mergedHeaders()
alg := mergedHeaders.getSignatureAlgorithm()
if !containsSignatureAlgorithm(signatureAlgorithms, alg) {
return nil, newErrUnexpectedSignatureAlgorithm(alg, signatureAlgorithms)
}
obj.Signatures[i].Header, err = mergedHeaders.sanitized()
if err != nil { if err != nil {
return nil, err return nil, err
} }
alg := SignatureAlgorithm(obj.Signatures[i].Header.Algorithm)
if !containsSignatureAlgorithm(signatureAlgorithms, alg) {
return nil, newErrUnexpectedSignatureAlgorithm(alg, signatureAlgorithms)
}
if obj.Signatures[i].header != nil { if obj.Signatures[i].header != nil {
obj.Signatures[i].Unprotected, err = obj.Signatures[i].header.sanitized() obj.Signatures[i].Unprotected, err = obj.Signatures[i].header.sanitized()
if err != nil { if err != nil {
@@ -387,6 +359,7 @@ func (parsed *rawJSONWebSignature) sanitized(signatureAlgorithms []SignatureAlgo
// Copy value of sig // Copy value of sig
original := sig original := sig
obj.Signatures[i].header = sig.Header
obj.Signatures[i].original = &original obj.Signatures[i].original = &original
} }
@@ -451,10 +424,7 @@ func (obj JSONWebSignature) compactSerialize(detached bool) (string, error) {
return "", ErrNotSupported return "", ErrNotSupported
} }
serializedProtected, err := json.Marshal(obj.Signatures[0].protected) serializedProtected := mustSerializeJSON(obj.Signatures[0].protected)
if err != nil {
return "", err
}
var payload []byte var payload []byte
if !detached { if !detached {
@@ -479,23 +449,14 @@ func (obj JSONWebSignature) DetachedCompactSerialize() (string, error) {
} }
// FullSerialize serializes an object using the full JSON serialization format. // FullSerialize serializes an object using the full JSON serialization format.
//
// It returns "" if obj cannot be serialized.
func (obj JSONWebSignature) FullSerialize() string { func (obj JSONWebSignature) FullSerialize() string {
raw := rawJSONWebSignature{ raw := rawJSONWebSignature{
Payload: newBuffer(obj.payload), Payload: newBuffer(obj.payload),
} }
if len(obj.Signatures) == 0 {
return ""
}
if len(obj.Signatures) == 1 { if len(obj.Signatures) == 1 {
if obj.Signatures[0].protected != nil { if obj.Signatures[0].protected != nil {
serializedProtected, err := json.Marshal(obj.Signatures[0].protected) serializedProtected := mustSerializeJSON(obj.Signatures[0].protected)
if err != nil {
return ""
}
raw.Protected = newBuffer(serializedProtected) raw.Protected = newBuffer(serializedProtected)
} }
raw.Header = obj.Signatures[0].header raw.Header = obj.Signatures[0].header
@@ -509,18 +470,10 @@ func (obj JSONWebSignature) FullSerialize() string {
} }
if signature.protected != nil { if signature.protected != nil {
serializedProtected, err := json.Marshal(signature.protected) raw.Signatures[i].Protected = newBuffer(mustSerializeJSON(signature.protected))
if err != nil {
return ""
}
raw.Signatures[i].Protected = newBuffer(serializedProtected)
} }
} }
} }
out, err := json.Marshal(raw) return string(mustSerializeJSON(raw))
if err != nil {
return ""
}
return string(out)
} }
+1 -1
View File
@@ -47,7 +47,7 @@ var ErrNotValidYet = errors.New("go-jose/go-jose/jwt: validation failed, token n
var ErrExpired = errors.New("go-jose/go-jose/jwt: validation failed, token is expired (exp)") var ErrExpired = errors.New("go-jose/go-jose/jwt: validation failed, token is expired (exp)")
// ErrIssuedInTheFuture indicates that the iat field is in the future. // ErrIssuedInTheFuture indicates that the iat field is in the future.
var ErrIssuedInTheFuture = errors.New("go-jose/go-jose/jwt: validation failed, token issued in the future (iat)") var ErrIssuedInTheFuture = errors.New("go-jose/go-jose/jwt: validation field, token issued in the future (iat)")
// ErrInvalidContentType indicates that token requires JWT cty header. // ErrInvalidContentType indicates that token requires JWT cty header.
var ErrInvalidContentType = errors.New("go-jose/go-jose/jwt: expected content type to be JWT (cty header)") var ErrInvalidContentType = errors.New("go-jose/go-jose/jwt: expected content type to be JWT (cty header)")
+17 -12
View File
@@ -85,12 +85,7 @@ func (t *NestedJSONWebToken) Decrypt(decryptionKey interface{}) (*JSONWebToken,
return sig, nil return sig, nil
} }
// ParseSigned parses a JWT in the JWS Compact Serialization. // ParseSigned parses token from JWS form.
//
// https://datatracker.ietf.org/doc/html/rfc7515#section-3.1
//
// The signatureAlgorithms parameter contains the allowable algorithms in the "alg" header parameter.
// It must be non-empty. Non-matching algorithms cause a parse error.
func ParseSigned(s string, signatureAlgorithms []jose.SignatureAlgorithm) (*JSONWebToken, error) { func ParseSigned(s string, signatureAlgorithms []jose.SignatureAlgorithm) (*JSONWebToken, error) {
sig, err := jose.ParseSignedCompact(s, signatureAlgorithms) sig, err := jose.ParseSignedCompact(s, signatureAlgorithms)
if err != nil { if err != nil {
@@ -147,10 +142,13 @@ func parseEncryptedCompact(
return enc, nil return enc, nil
} }
// ParseEncrypted parses a JWT in the JWE Compact Serialization. // ParseEncrypted parses token from JWE form.
// //
// The keyAlgorithms and contentEncryption parameters contain the allowable algorithms in the "alg" and "enc" // The keyAlgorithms and contentEncryption parameters are used to validate the "alg" and "enc"
// header parameters respectively. They must be non-empty. Non-matching algorithms cause a parse error. // header parameters respectively. They must be nonempty, and each "alg" or "enc" header in
// parsed data must contain a value that is present in the corresponding parameter. That
// includes the protected and unprotected headers as well as all recipients. To accept
// multiple algorithms, pass a slice of all the algorithms you want to accept.
func ParseEncrypted(s string, func ParseEncrypted(s string,
keyAlgorithms []jose.KeyAlgorithm, keyAlgorithms []jose.KeyAlgorithm,
contentEncryption []jose.ContentEncryption, contentEncryption []jose.ContentEncryption,
@@ -166,10 +164,17 @@ func ParseEncrypted(s string,
}, nil }, nil
} }
// ParseSignedAndEncrypted parses a signed-then-encrypted token in the JWE Compact Serialization. // ParseSignedAndEncrypted parses signed-then-encrypted token from JWE form.
// //
// The encryptionKeyAlgorithms, contentEncryption, and signatureAlgorithms parameters contain the allowable // The encryptionKeyAlgorithms and contentEncryption parameters are used to validate the "alg" and "enc"
// algorithms. They must be non-empty. Non-matching algorithms cause a parse error. // header parameters, respectively, of the outer JWE. They must be nonempty, and each "alg" or "enc"
// header in parsed data must contain a value that is present in the corresponding parameter. That
// includes the protected and unprotected headers as well as all recipients. To accept
// multiple algorithms, pass a slice of all the algorithms you want to accept.
//
// The signatureAlgorithms parameter is used to validate the "alg" header parameter of the
// inner JWS. It must be nonempty, and the "alg" header in the inner JWS must contain a value
// that is present in the parameter.
func ParseSignedAndEncrypted(s string, func ParseSignedAndEncrypted(s string,
encryptionKeyAlgorithms []jose.KeyAlgorithm, encryptionKeyAlgorithms []jose.KeyAlgorithm,
contentEncryption []jose.ContentEncryption, contentEncryption []jose.ContentEncryption,
+37 -43
View File
@@ -18,7 +18,6 @@ package jose
import ( import (
"crypto/elliptic" "crypto/elliptic"
"crypto/rsa"
"crypto/x509" "crypto/x509"
"encoding/base64" "encoding/base64"
"errors" "errors"
@@ -211,20 +210,13 @@ type Header struct {
// Certificates verifies & returns the certificate chain present // Certificates verifies & returns the certificate chain present
// in the x5c header field of a message, if one was present. Returns // in the x5c header field of a message, if one was present. Returns
// an error if there was no x5c header present, any certificate in the // an error if there was no x5c header present or the chain could
// chain has an RSA key larger than [MaxRSAModulusBits], or the chain could
// not be validated with the given verify options. // not be validated with the given verify options.
func (h Header) Certificates(opts x509.VerifyOptions) ([][]*x509.Certificate, error) { func (h Header) Certificates(opts x509.VerifyOptions) ([][]*x509.Certificate, error) {
if len(h.certificates) == 0 { if len(h.certificates) == 0 {
return nil, ErrMissingX5cHeader return nil, ErrMissingX5cHeader
} }
for _, cert := range h.certificates {
if pub, ok := cert.PublicKey.(*rsa.PublicKey); ok && pub.N.BitLen() > MaxRSAModulusBits {
return nil, fmt.Errorf("go-jose/go-jose: invalid RSA key in x5c certificate, modulus too large")
}
}
leaf := h.certificates[0] leaf := h.certificates[0]
if opts.Intermediates == nil { if opts.Intermediates == nil {
opts.Intermediates = x509.NewCertPool() opts.Intermediates = x509.NewCertPool()
@@ -305,10 +297,9 @@ func (parsed rawHeader) getEPK() (*JSONWebKey, error) {
if v == nil { if v == nil {
return nil, nil return nil, nil
} }
// RFC 7518 Section 4.6.1.1: the "epk" header is the ephemeral public key var epk *JSONWebKey
// of the originator. Reject private key material before parsing it. err := json.Unmarshal(*v, &epk)
epk := new(JSONWebKey) if err != nil {
if err := epk.unmarshalJSON(*v, false); err != nil {
return nil, err return nil, err
} }
return epk, nil return epk, nil
@@ -334,6 +325,20 @@ func (parsed rawHeader) getTag() (*byteBuffer, error) {
return parsed.getByteBuffer(headerTag) return parsed.getByteBuffer(headerTag)
} }
// getJWK extracts parsed "jwk" from the raw JSON.
func (parsed rawHeader) getJWK() (*JSONWebKey, error) {
v := parsed[headerJWK]
if v == nil {
return nil, nil
}
var jwk *JSONWebKey
err := json.Unmarshal(*v, &jwk)
if err != nil {
return nil, err
}
return jwk, nil
}
// getCritical extracts parsed "crit" from the raw JSON. If omitted, it // getCritical extracts parsed "crit" from the raw JSON. If omitted, it
// returns an empty slice. // returns an empty slice.
func (parsed rawHeader) getCritical() ([]string, error) { func (parsed rawHeader) getCritical() ([]string, error) {
@@ -419,14 +424,10 @@ func (parsed rawHeader) sanitized() (h Header, err error) {
} }
switch k { switch k {
case headerJWK: case headerJWK:
// RFC 7515 Section 4.1.3 and RFC 7516 Section 4.1.5: the "jwk" var jwk *JSONWebKey
// header is the public key that corresponds to the key used to err = json.Unmarshal(*v, &jwk)
// sign the JWS or encrypt the JWE. Reject private key material
// before parsing it.
jwk := new(JSONWebKey)
err = jwk.unmarshalJSON(*v, false)
if err != nil { if err != nil {
err = fmt.Errorf("failed to unmarshal JWK: %w", err) err = fmt.Errorf("failed to unmarshal JWK: %v: %#v", err, string(*v))
return return
} }
h.JSONWebKey = jwk h.JSONWebKey = jwk
@@ -498,8 +499,22 @@ func parseCertificateChain(chain []string) ([]*x509.Certificate, error) {
} }
func (parsed rawHeader) isSet(k HeaderKey) bool { func (parsed rawHeader) isSet(k HeaderKey) bool {
_, ok := parsed[k] dvr := parsed[k]
return ok if dvr == nil {
return false
}
var dv interface{}
err := json.Unmarshal(*dvr, &dv)
if err != nil {
return true
}
if dvStr, ok := dv.(string); ok {
return dvStr != ""
}
return true
} }
// Merge headers from src into dst, giving precedence to headers from l. // Merge headers from src into dst, giving precedence to headers from l.
@@ -549,24 +564,3 @@ func makeRawMessage(b []byte) *json.RawMessage {
rm := json.RawMessage(b) rm := json.RawMessage(b)
return &rm return &rm
} }
// parseProtectedHeader parses a `byteBuffer` into a `*rawHeader`.
//
// The byteBuffer represents a base64-decoded "protected" header, which
// is specified to contain a JSON object.
//
// If the input is nil or empty, returns `nil, nil`.
//
// `rawHeader` is a map type and so can be nil. When this function
// returns a non-nil `*rawHeader`, the map is guaranteed to be non-nil.
func parseProtectedHeader(in *byteBuffer) (*rawHeader, error) {
if in == nil || len(in.bytes()) == 0 {
return nil, nil
}
var header rawHeader
err := json.Unmarshal(in.bytes(), &header)
if err != nil || header == nil {
return nil, errors.New("go-jose/go-jose: invalid protected header")
}
return &header, nil
}
+1 -4
View File
@@ -321,10 +321,7 @@ func (ctx *genericSigner) Sign(payload []byte) (*JSONWebSignature, error) {
protected[k] = v protected[k] = v
} }
serializedProtected, err := json.Marshal(protected) serializedProtected := mustSerializeJSON(protected)
if err != nil {
return nil, fmt.Errorf("go-jose/go-jose: Error marshaling JSON: %s", err)
}
needsBase64 := true needsBase64 := true
if b64, ok := protected[headerB64]; ok { if b64, ok := protected[headerB64]; ok {
+1 -1
View File
@@ -421,7 +421,7 @@ func (ctx *symmetricKeyCipher) decryptKey(headers rawHeader, recipient *recipien
if err != nil { if err != nil {
return nil, fmt.Errorf("go-jose/go-jose: invalid P2S: %v", err) return nil, fmt.Errorf("go-jose/go-jose: invalid P2S: %v", err)
} }
if p2s == nil || len(p2s.bytes()) == 0 { if p2s == nil || len(p2s.data) == 0 {
return nil, fmt.Errorf("go-jose/go-jose: invalid P2S: must be present") return nil, fmt.Errorf("go-jose/go-jose: invalid P2S: must be present")
} }
-13
View File
@@ -1,13 +0,0 @@
# This containerfile can be used to run the go-jose tests and cli
FROM golang:1.27-trixie
ENV GOTOOLCHAIN=local
COPY . /src
WORKDIR /src
# Install the jose-util cli, which will also build the library
RUN go build -o /usr/local/bin/jose-util ./jose-util
# By default, run the tests
CMD ["go", "test", "./..."]
-25
View File
@@ -1,25 +0,0 @@
# Threat model
## What this project does and where untrusted input enters
go-jose is a Go implementation of JOSE family of algorithms.
It is a library which will parse attacker-controlled data in network-reachable
services.
## Components that matter most / least
The primary attack surface of interest is parsing and validating JWS, JWE, JWK,
etc when this library is used to implement authentication and authorization.
## How to exercise it
Testing is primarily done by Golang unit tests, which can be run with the
tests.Container file's default CMD.
## How you rate severity
Any authentication bypass enabled by a go-jose issue is a high+ severity.
Denial-of-service attacks are of interest when the functions may be used in a protocol like OIDC, ACME, etc
which could have a server processing attacker-controlled data.
## Anything to leave alone
Any bug which only affects the jose-util CLI tool, but it can be used to exercise the library.
-2
View File
@@ -6,9 +6,7 @@ This package provides various compression algorithms.
* [S2](https://github.com/klauspost/compress/tree/master/s2#s2-compression) is a high performance replacement for Snappy. * [S2](https://github.com/klauspost/compress/tree/master/s2#s2-compression) is a high performance replacement for Snappy.
* Optimized [deflate](https://godoc.org/github.com/klauspost/compress/flate) packages which can be used as a dropin replacement for [gzip](https://godoc.org/github.com/klauspost/compress/gzip), [zip](https://godoc.org/github.com/klauspost/compress/zip) and [zlib](https://godoc.org/github.com/klauspost/compress/zlib). * Optimized [deflate](https://godoc.org/github.com/klauspost/compress/flate) packages which can be used as a dropin replacement for [gzip](https://godoc.org/github.com/klauspost/compress/gzip), [zip](https://godoc.org/github.com/klauspost/compress/zip) and [zlib](https://godoc.org/github.com/klauspost/compress/zlib).
* [snappy](https://github.com/klauspost/compress/tree/master/snappy) is a drop-in replacement for `github.com/golang/snappy` offering better compression and concurrent streams. * [snappy](https://github.com/klauspost/compress/tree/master/snappy) is a drop-in replacement for `github.com/golang/snappy` offering better compression and concurrent streams.
* [lzw](https://github.com/klauspost/compress/tree/master/lzw) is a drop-in replacement for `compress/lzw` with 1.4-4x faster decompression and 1.1-2.7x faster compression, depending on the data.
* [huff0](https://github.com/klauspost/compress/tree/master/huff0) and [FSE](https://github.com/klauspost/compress/tree/master/fse) implementations for raw entropy encoding. * [huff0](https://github.com/klauspost/compress/tree/master/huff0) and [FSE](https://github.com/klauspost/compress/tree/master/fse) implementations for raw entropy encoding.
* [Xpress](https://github.com/klauspost/compress/tree/master/xpress) decompression of the Microsoft XPRESS (MS-XCA) plain LZ77 and LZ77+Huffman formats (the LZ77+Huffman variant is the one used in WIM images and Windows Compact OS / WOF data).
* [gzhttp](https://github.com/klauspost/compress/tree/master/gzhttp) Provides client and server wrappers for handling gzipped/zstd HTTP requests efficiently. * [gzhttp](https://github.com/klauspost/compress/tree/master/gzhttp) Provides client and server wrappers for handling gzipped/zstd HTTP requests efficiently.
* [pgzip](https://github.com/klauspost/pgzip) is a separate package that provides a very fast parallel gzip implementation. * [pgzip](https://github.com/klauspost/pgzip) is a separate package that provides a very fast parallel gzip implementation.
+1 -2
View File
@@ -659,8 +659,7 @@ func (s *Scratch) validateNorm() (err error) {
if v >= 0 { if v >= 0 {
total += int(v) total += int(v)
} else { } else {
// -1 takes one cell. Adding -v would let a wrapped -32768 pass. total -= int(v)
total++
} }
} }
defer func() { defer func() {
-6
View File
@@ -3,7 +3,6 @@ package fse
import ( import (
"errors" "errors"
"fmt" "fmt"
"math"
) )
const ( const (
@@ -124,11 +123,6 @@ func (s *Scratch) readNCount() error {
remaining -= count remaining -= count
gotTotal += count gotTotal += count
} }
if count > math.MaxInt16 {
// Only one symbol filling a tableLog 15 table, which cannot be decoded.
// Truncated, it would leave the decoding table unwritten.
return fmt.Errorf("corruption detected (count %d > %d)", count, math.MaxInt16)
}
s.norm[charnum&0xff] = int16(count) s.norm[charnum&0xff] = int16(count)
charnum++ charnum++
previous0 = count == 0 previous0 = count == 0
-63
View File
@@ -9,7 +9,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"math/bits"
"github.com/klauspost/compress/internal/le" "github.com/klauspost/compress/internal/le"
) )
@@ -211,68 +210,6 @@ func (b *bitReaderShifted) remaining() uint {
return b.off*8 + uint(64-b.bitsRead) return b.off*8 + uint(64-b.bitsRead)
} }
// canUseAsm reports whether the reader has a full 8-byte window ahead of
// its read pointer, which the Decompress4X asm loops need to take over.
func (b *bitReaderShifted) canUseAsm() bool {
return b.off >= 8
}
// prepareForAsm establishes the invariant the Decompress4X asm loops rely
// on: value == load64(in[off:off+8]) << bitsRead with bitsRead <= 7, so
// that a full group of symbols can never shift their sentinel bit out of
// the container. init leaves bitsRead == 8 when the final byte of the
// stream is exactly 0x01; that whole byte is consumed, so the same position
// is the window one byte lower with nothing consumed. Requires canUseAsm.
func (b *bitReaderShifted) prepareForAsm() {
if b.bitsRead >= 8 {
b.off--
b.value = le.Load64(b.in, b.off)
b.bitsRead -= 8
}
}
// restoreFromAsm converts the state left behind by the Decompress4X asm
// loops back to the invariant the Go code relies on: value holds the 8
// bytes at in[off:off+8] shifted left by bitsRead, with the low bitsRead
// bits zero.
//
// The asm keeps a sentinel bit in value just below the unread bits, so its
// trailing zero count is the number of consumed bits. The sentinel is ORed
// over the lowest bit of the window, which the loop never consumes before
// re-reading memory, so the window is re-read here too rather than taken
// from value. The asm reports off as the signed distance from the start of
// the stream, and it can be negative: a reload always reads a whole 8-byte
// window, so a stream that is nearly drained ends with up to 7 bytes of the
// previous stream (or the jump table) below its start inside the window.
// Those bytes sit below the stream's own bits and count as consumed.
// Anything further below, or more bits consumed than the stream holds, is
// corruption.
func (b *bitReaderShifted) restoreFromAsm() error {
off := int(b.off)
consumed := uint(bits.TrailingZeros64(b.value))
if off < 0 {
if off < -7 {
return errors.New("corruption detected: stream underrun")
}
consumed += uint(-off) * 8
if consumed > 64 {
return errors.New("corruption detected: stream underrun")
}
off = 0
}
if off+8 > len(b.in) {
return errors.New("corruption detected: stream overrun")
}
b.off = uint(off)
b.bitsRead = uint8(consumed)
if consumed >= 64 {
b.value = 0
} else {
b.value = le.Load64(b.in, b.off) << consumed
}
return nil
}
// close the bitstream and returns an error if out-of-buffer reads occurred. // close the bitstream and returns an error if out-of-buffer reads occurred.
func (b *bitReaderShifted) close() error { func (b *bitReaderShifted) close() error {
// Release reference. // Release reference.
-4
View File
@@ -54,10 +54,6 @@ func ReadTable(in []byte, s *Scratch) (s2 *Scratch, remain []byte, err error) {
if len(in) < int(iSize) { if len(in) < int(iSize) {
return s, nil, fmt.Errorf("input too small for table, want %d bytes, have %d", iSize, len(in)) return s, nil, fmt.Errorf("input too small for table, want %d bytes, have %d", iSize, len(in))
} }
// The low nibble is the weights' FSE tableLog - 5, which the format limits to 6.
if in[0]&15+5 > 6 {
return s, nil, errors.New("corrupt input: weights tableLog too big")
}
// FSE compressed weights // FSE compressed weights
s.fse.DecompressLimit = 255 s.fse.DecompressLimit = 255
hw := s.huffWeight[:] hw := s.huffWeight[:]
+8 -87
View File
@@ -8,99 +8,36 @@ import (
) )
// decompress4x_main_loop_amd64 is an x86 assembler implementation // decompress4x_main_loop_amd64 is an x86 assembler implementation
// of Decompress4X when tablelog > 8, decoding fastSymbols symbols per // of Decompress4X when tablelog > 8.
// stream between bit container reloads.
// //
//go:noescape //go:noescape
func decompress4x_main_loop_amd64(ctx *decompress4xContext) func decompress4x_main_loop_amd64(ctx *decompress4xContext)
// decompress4x_8b_main_loop_amd64 is an x86 assembler implementation // decompress4x_8b_main_loop_amd64 is an x86 assembler implementation
// of Decompress4X when tablelog <= 8, decoding fast8bSymbols symbols // of Decompress4X when tablelog <= 8 which decodes 4 entries
// per stream between bit container reloads. // per loop.
// //
//go:noescape //go:noescape
func decompress4x_8b_main_loop_amd64(ctx *decompress4xContext) func decompress4x_8b_main_loop_amd64(ctx *decompress4xContext)
// decompress4x_4b_main_loop_amd64 is an x86 assembler implementation
// of Decompress4X when tablelog <= 4, decoding fast4bSymbols symbols
// per stream between bit container reloads.
//
//go:noescape
func decompress4x_4b_main_loop_amd64(ctx *decompress4xContext)
// decompress4x_4b_main_loop_bmi2 is the BMI2 twin of decompress4x_4b_main_loop_amd64.
//
//go:noescape
func decompress4x_4b_main_loop_bmi2(ctx *decompress4xContext)
// decompress4x_main_loop_bmi2 is the BMI2 twin of decompress4x_main_loop_amd64.
//
//go:noescape
func decompress4x_main_loop_bmi2(ctx *decompress4xContext)
// decompress4x_8b_main_loop_bmi2 is the BMI2 twin of decompress4x_8b_main_loop_amd64.
//
//go:noescape
func decompress4x_8b_main_loop_bmi2(ctx *decompress4xContext)
// decompress1x_main_loop_amd64 is an x86 assembler implementation // decompress1x_main_loop_amd64 is an x86 assembler implementation
// of Decompress1X when tablelog > 8, decoding fastSymbols symbols // of Decompress1X when tablelog > 8.
// between bit container reloads.
// //
//go:noescape //go:noescape
func decompress1x_main_loop_amd64(ctx *decompress1xContext) func decompress1x_main_loop_amd64(ctx *decompress1xContext)
// decompress1x_8b_main_loop_amd64 is an x86 assembler implementation // decompress1x_main_loop_bmi2 is an x86 with BMI2 assembler implementation
// of Decompress1X when tablelog <= 8, decoding fast8bSymbols symbols // of Decompress1X when tablelog > 8.
// between bit container reloads.
//
//go:noescape
func decompress1x_8b_main_loop_amd64(ctx *decompress1xContext)
// decompress1x_4b_main_loop_amd64 is an x86 assembler implementation
// of Decompress1X when tablelog <= 4, decoding fast4bSymbols symbols
// between bit container reloads.
//
//go:noescape
func decompress1x_4b_main_loop_amd64(ctx *decompress1xContext)
// decompress1x_main_loop_bmi2 is the BMI2 twin of decompress1x_main_loop_amd64.
// //
//go:noescape //go:noescape
func decompress1x_main_loop_bmi2(ctx *decompress1xContext) func decompress1x_main_loop_bmi2(ctx *decompress1xContext)
// decompress1x_8b_main_loop_bmi2 is the BMI2 twin of decompress1x_8b_main_loop_amd64.
//
//go:noescape
func decompress1x_8b_main_loop_bmi2(ctx *decompress1xContext)
// decompress1x_4b_main_loop_bmi2 is the BMI2 twin of decompress1x_4b_main_loop_amd64.
//
//go:noescape
func decompress1x_4b_main_loop_bmi2(ctx *decompress1xContext)
func decompress4x_main_loop_asm(ctx *decompress4xContext) { func decompress4x_main_loop_asm(ctx *decompress4xContext) {
if cpuinfo.HasBMI2() { decompress4x_main_loop_amd64(ctx)
decompress4x_main_loop_bmi2(ctx)
} else {
decompress4x_main_loop_amd64(ctx)
}
} }
func decompress4x_8b_main_loop_asm(ctx *decompress4xContext) { func decompress4x_8b_main_loop_asm(ctx *decompress4xContext) {
if cpuinfo.HasBMI2() { decompress4x_8b_main_loop_amd64(ctx)
decompress4x_8b_main_loop_bmi2(ctx)
} else {
decompress4x_8b_main_loop_amd64(ctx)
}
}
func decompress4x_4b_main_loop_asm(ctx *decompress4xContext) {
if cpuinfo.HasBMI2() {
decompress4x_4b_main_loop_bmi2(ctx)
} else {
decompress4x_4b_main_loop_amd64(ctx)
}
} }
func decompress1x_main_loop_asm(ctx *decompress1xContext) { func decompress1x_main_loop_asm(ctx *decompress1xContext) {
@@ -110,19 +47,3 @@ func decompress1x_main_loop_asm(ctx *decompress1xContext) {
decompress1x_main_loop_amd64(ctx) decompress1x_main_loop_amd64(ctx)
} }
} }
func decompress1x_8b_main_loop_asm(ctx *decompress1xContext) {
if cpuinfo.HasBMI2() {
decompress1x_8b_main_loop_bmi2(ctx)
} else {
decompress1x_8b_main_loop_amd64(ctx)
}
}
func decompress1x_4b_main_loop_asm(ctx *decompress1xContext) {
if cpuinfo.HasBMI2() {
decompress1x_4b_main_loop_bmi2(ctx)
} else {
decompress1x_4b_main_loop_amd64(ctx)
}
}
File diff suppressed because it is too large Load Diff
+4 -39
View File
@@ -6,47 +6,24 @@
package huff0 package huff0
// decompress4x_main_loop_arm64 is an arm64 assembler implementation // decompress4x_main_loop_arm64 is an arm64 assembler implementation
// of Decompress4X when tablelog > 8, decoding fastSymbols symbols per // of Decompress4X when tablelog > 8.
// stream between bit container reloads.
// //
//go:noescape //go:noescape
func decompress4x_main_loop_arm64(ctx *decompress4xContext) func decompress4x_main_loop_arm64(ctx *decompress4xContext)
// decompress4x_8b_main_loop_arm64 is an arm64 assembler implementation // decompress4x_8b_main_loop_arm64 is an arm64 assembler implementation
// of Decompress4X when tablelog <= 8, decoding fast8bSymbols symbols // of Decompress4X when tablelog <= 8 which decodes 4 entries
// per stream between bit container reloads. // per loop.
// //
//go:noescape //go:noescape
func decompress4x_8b_main_loop_arm64(ctx *decompress4xContext) func decompress4x_8b_main_loop_arm64(ctx *decompress4xContext)
// decompress4x_4b_main_loop_arm64 is an arm64 assembler implementation
// of Decompress4X when tablelog <= 4, decoding fast4bSymbols symbols
// per stream between bit container reloads.
//
//go:noescape
func decompress4x_4b_main_loop_arm64(ctx *decompress4xContext)
// decompress1x_main_loop_arm64 is an arm64 assembler implementation // decompress1x_main_loop_arm64 is an arm64 assembler implementation
// of Decompress1X when tablelog > 8, decoding fastSymbols symbols // of Decompress1X when tablelog > 8.
// between bit container reloads.
// //
//go:noescape //go:noescape
func decompress1x_main_loop_arm64(ctx *decompress1xContext) func decompress1x_main_loop_arm64(ctx *decompress1xContext)
// decompress1x_8b_main_loop_arm64 is an arm64 assembler implementation
// of Decompress1X when tablelog <= 8, decoding fast8bSymbols symbols
// between bit container reloads.
//
//go:noescape
func decompress1x_8b_main_loop_arm64(ctx *decompress1xContext)
// decompress1x_4b_main_loop_arm64 is an arm64 assembler implementation
// of Decompress1X when tablelog <= 4, decoding fast4bSymbols symbols
// between bit container reloads.
//
//go:noescape
func decompress1x_4b_main_loop_arm64(ctx *decompress1xContext)
func decompress4x_main_loop_asm(ctx *decompress4xContext) { func decompress4x_main_loop_asm(ctx *decompress4xContext) {
decompress4x_main_loop_arm64(ctx) decompress4x_main_loop_arm64(ctx)
} }
@@ -55,18 +32,6 @@ func decompress4x_8b_main_loop_asm(ctx *decompress4xContext) {
decompress4x_8b_main_loop_arm64(ctx) decompress4x_8b_main_loop_arm64(ctx)
} }
func decompress4x_4b_main_loop_asm(ctx *decompress4xContext) {
decompress4x_4b_main_loop_arm64(ctx)
}
func decompress1x_main_loop_asm(ctx *decompress1xContext) { func decompress1x_main_loop_asm(ctx *decompress1xContext) {
decompress1x_main_loop_arm64(ctx) decompress1x_main_loop_arm64(ctx)
} }
func decompress1x_8b_main_loop_asm(ctx *decompress1xContext) {
decompress1x_8b_main_loop_arm64(ctx)
}
func decompress1x_4b_main_loop_asm(ctx *decompress1xContext) {
decompress1x_4b_main_loop_arm64(ctx)
}
File diff suppressed because it is too large Load Diff
+21 -81
View File
@@ -14,10 +14,6 @@ import (
// fallback8BitSize is the size where using Go version is faster. // fallback8BitSize is the size where using Go version is faster.
const fallback8BitSize = 800 const fallback8BitSize = 800
// decompress4xContext is the argument block of the Decompress4X asm loops.
// Go fills every field but decoded and inner; the asm advances ip, and on
// return leaves each bit reader in the form bitReaderShifted.restoreFromAsm
// expects.
type decompress4xContext struct { type decompress4xContext struct {
pbr *[4]bitReaderShifted pbr *[4]bitReaderShifted
peekBits uint8 peekBits uint8
@@ -25,20 +21,9 @@ type decompress4xContext struct {
dstEvery int dstEvery int
tbl *dEntrySingle tbl *dEntrySingle
decoded int decoded int
limit *byte // stream 0's output pointer must stay below this limit *byte
ilowest *byte // start of the input block; no read goes below it
ip [4]*byte // each stream's 8-byte input window, advanced by the asm
inner *byte // scratch for the asm: the current inner-loop limit
} }
// Symbols decoded per stream between reloads by the 4X asm loops; must
// match the constants of the same names in _generate/gen.go.
const (
fastSymbols = 5 // tablelog 9..11
fast8bSymbols = 7 // tablelog 5..8
fast4bSymbols = 14 // tablelog <= 4
)
// Decompress4X will decompress a 4X encoded stream. // Decompress4X will decompress a 4X encoded stream.
// The length of the supplied input must match the end of a block exactly. // The length of the supplied input must match the end of a block exactly.
// The *capacity* of the dst slice must match the destination size of // The *capacity* of the dst slice must match the destination size of
@@ -87,55 +72,23 @@ func (d *Decoder) Decompress4X(dst, src []byte) ([]byte, error) {
var decoded int var decoded int
nSyms := fastSymbols if len(out) > 4*4 && !(br[0].off < 4 || br[1].off < 4 || br[2].off < 4 || br[3].off < 4) {
if d.actualTableLog <= 4 {
nSyms = fast4bSymbols
} else if use8BitTables {
nSyms = fast8bSymbols
}
// The asm writes nSyms bytes per stream per iteration and only re-checks
// its bounds between batches of iterations (the batch size is derived
// from limit in _generate/gen.go), so stream 0 must stop early enough
// that stream 3 (which may be up to 3 bytes shorter than dstEvery)
// never writes past the end of out. Every stream needs a full 8-byte
// window ahead of its read pointer to enter the loop.
if limit := dstEvery - nSyms - 2; limit > 0 && br[0].canUseAsm() && br[1].canUseAsm() && br[2].canUseAsm() && br[3].canUseAsm() {
for i := range br {
br[i].prepareForAsm()
}
ctx := decompress4xContext{ ctx := decompress4xContext{
pbr: &br, pbr: &br,
peekBits: uint8((64 - d.actualTableLog) & 63), // see: bitReaderShifted.peekBitsFast() peekBits: uint8((64 - d.actualTableLog) & 63), // see: bitReaderShifted.peekBitsFast()
out: &out[0], out: &out[0],
dstEvery: dstEvery, dstEvery: dstEvery,
tbl: &single[0], tbl: &single[0],
limit: &out[limit], limit: &out[dstEvery-4], // Always stop decoding when first buffer gets here to avoid writing OOB on last.
// The 6-byte jump table sits below stream 0 inside src, so the
// lowest window may reach into it; restoreFromAsm accounts for
// bytes below a stream's start. Bounding by src rather than by
// stream 0 keeps the asm running about six bytes longer, which
// matters for streams with very short codes.
ilowest: &src[0],
} }
for i := range br { if use8BitTables {
ctx.ip[i] = &br[i].in[br[i].off]
}
switch nSyms {
case fast4bSymbols:
decompress4x_4b_main_loop_asm(&ctx)
case fast8bSymbols:
decompress4x_8b_main_loop_asm(&ctx) decompress4x_8b_main_loop_asm(&ctx)
default: } else {
decompress4x_main_loop_asm(&ctx) decompress4x_main_loop_asm(&ctx)
} }
decoded = ctx.decoded decoded = ctx.decoded
out = out[decoded/4:] out = out[decoded/4:]
for i := range br {
if err := br[i].restoreFromAsm(); err != nil {
return nil, err
}
}
} }
// Decode remaining. // Decode remaining.
@@ -175,20 +128,18 @@ func (d *Decoder) Decompress4X(dst, src []byte) ([]byte, error) {
return dst, nil return dst, nil
} }
// decompress1xContext is the argument block of the Decompress1X asm loops.
// Go fills every field but decoded; the asm advances ip, and on return
// leaves the bit reader in the form bitReaderShifted.restoreFromAsm expects.
type decompress1xContext struct { type decompress1xContext struct {
pbr *bitReaderShifted pbr *bitReaderShifted
peekBits uint8 peekBits uint8
out *byte out *byte
outCap int // no write reaches out[outCap] outCap int
tbl *dEntrySingle tbl *dEntrySingle
decoded int decoded int
ilowest *byte // start of the stream; no read goes below it
ip *byte // the 8-byte input window, advanced by the asm
} }
// Error reported by asm implementations
const error_max_decoded_size_exeeded = -1
// Decompress1X will decompress a 1X encoded stream. // Decompress1X will decompress a 1X encoded stream.
// The cap of the output buffer will be the maximum decompressed size. // The cap of the output buffer will be the maximum decompressed size.
// The length of the supplied input must match the end of a block exactly. // The length of the supplied input must match the end of a block exactly.
@@ -207,36 +158,25 @@ func (d *Decoder) Decompress1X(dst, src []byte) ([]byte, error) {
const tlSize = 1 << tableLogMax const tlSize = 1 << tableLogMax
const tlMask = tlSize - 1 const tlMask = tlSize - 1
// The asm decodes whole batches of symbols and only re-checks its if maxDecodedSize >= 4 {
// bounds between them, so it needs at least one batch of room (the
// output bound rounds nSyms up to 16, see _generate/gen.go) and a full
// 8-byte window ahead of the read pointer to enter the loop. It also
// needs at most 7 bits consumed on entry so that a batch cannot run
// the container dry, which prepareForAsm establishes.
decoded := 0
if maxDecodedSize >= 16 && br.canUseAsm() {
br.prepareForAsm()
ctx := decompress1xContext{ ctx := decompress1xContext{
pbr: &br, pbr: &br,
out: &dst[0], out: &dst[0],
outCap: maxDecodedSize, outCap: maxDecodedSize,
peekBits: uint8((64 - d.actualTableLog) & 63), // see: bitReaderShifted.peekBitsFast() peekBits: uint8((64 - d.actualTableLog) & 63), // see: bitReaderShifted.peekBitsFast()
tbl: &d.dt.single[0], tbl: &d.dt.single[0],
ilowest: &src[0],
ip: &br.in[br.off],
} }
if d.actualTableLog <= 4 {
decompress1x_4b_main_loop_asm(&ctx)
} else if d.actualTableLog <= 8 {
decompress1x_8b_main_loop_asm(&ctx)
} else {
decompress1x_main_loop_asm(&ctx)
}
decoded = ctx.decoded
}
dst = dst[:decoded]
bitsLeft := br.remaining() decompress1x_main_loop_asm(&ctx)
if ctx.decoded == error_max_decoded_size_exeeded {
return nil, ErrMaxDecodedSizeExceeded
}
dst = dst[:ctx.decoded]
}
// br < 8, so uint8 is fine
bitsLeft := uint8(br.off)*8 + 64 - br.bitsRead
for bitsLeft > 0 { for bitsLeft > 0 {
br.fill() br.fill()
if len(dst) >= maxDecodedSize { if len(dst) >= maxDecodedSize {
@@ -246,7 +186,7 @@ func (d *Decoder) Decompress1X(dst, src []byte) ([]byte, error) {
v := d.dt.single[br.peekBitsFast(d.actualTableLog)&tlMask] v := d.dt.single[br.peekBitsFast(d.actualTableLog)&tlMask]
nBits := uint8(v.entry) nBits := uint8(v.entry)
br.advance(nBits) br.advance(nBits)
bitsLeft -= uint(nBits) bitsLeft -= nBits
dst = append(dst, uint8(v.entry>>8)) dst = append(dst, uint8(v.entry>>8))
} }
return dst, br.close() return dst, br.close()
-3
View File
@@ -240,11 +240,9 @@ func (b *blockDec) decodeBuf(hist *history) error {
b.dst[i] = v b.dst[i] = v
} }
hist.appendKeep(b.dst) hist.appendKeep(b.dst)
hist.decoders.consumeSyncLen(len(b.dst))
return nil return nil
case blockTypeRaw: case blockTypeRaw:
hist.appendKeep(b.data) hist.appendKeep(b.data)
hist.decoders.consumeSyncLen(len(b.data))
return nil return nil
case blockTypeCompressed: case blockTypeCompressed:
saved := b.dst saved := b.dst
@@ -489,7 +487,6 @@ func (b *blockDec) decodeCompressed(hist *history) error {
} }
if hist.decoders.nSeqs == 0 { if hist.decoders.nSeqs == 0 {
b.dst = append(b.dst, hist.decoders.literals...) b.dst = append(b.dst, hist.decoders.literals...)
hist.decoders.consumeSyncLen(len(hist.decoders.literals))
return nil return nil
} }
before := len(hist.decoders.out) before := len(hist.decoders.out)

Some files were not shown because too many files have changed in this diff Show More