mirror of
https://github.com/docker/cli.git
synced 2026-11-07 13:32:59 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a82e528901 | ||
|
|
e35bc1b6ae | ||
|
|
baf6c13889 | ||
|
|
4051f75f69 | ||
|
|
76321a699d | ||
|
|
b82456c62f | ||
|
|
a98becc3e3 | ||
|
|
dfc520dcc2 | ||
|
|
a3a2bb6118 | ||
|
|
c11295afc4 | ||
|
|
010e125d8b | ||
|
|
5cdf229230 | ||
|
|
66f6797582 | ||
|
|
cb8870a537 | ||
|
|
529f66d2f9 | ||
|
|
4d6ff7d646 | ||
|
|
b997c5b7aa | ||
|
|
1822903c1d | ||
|
|
37013cb262 | ||
|
|
61ee187552 | ||
|
|
433b13a249 | ||
|
|
5c8bfb2844 | ||
|
|
528a2ada2e | ||
|
|
de8f4032ad | ||
|
|
e4078058de | ||
|
|
8da8b00555 | ||
|
|
067d069f0c | ||
|
|
02fbbf3eb0 | ||
|
|
890976a13b | ||
|
|
23c66ccd61 | ||
|
|
3e686a8b59 | ||
|
|
f415da838b | ||
|
|
45c2317672 | ||
|
|
8308969eda | ||
|
|
cf809998ab | ||
|
|
730c880b35 | ||
|
|
e63e4fe498 | ||
|
|
e7df01fe6e | ||
|
|
2e51107855 | ||
|
|
949bf401dd | ||
|
|
9db9802127 | ||
|
|
63445d6b70 | ||
|
|
fdb4ee3e75 | ||
|
|
4ae247756e | ||
|
|
e7c0f5c53c | ||
|
|
87fb2ebe7e | ||
|
|
e8c098c8bf | ||
|
|
374636ff15 | ||
|
|
862e7e6ad4 | ||
|
|
3d2650ce76 | ||
|
|
e11705385d | ||
|
|
fd58705f3a | ||
|
|
5e6d8e3ab6 | ||
|
|
8503a98b68 | ||
|
|
488292ca1f | ||
|
|
75d1d1a9e6 | ||
|
|
cb192aeb41 | ||
|
|
6cb63efe6f | ||
|
|
48988f5c12 | ||
|
|
e3559d29fb | ||
|
|
4f8ad7605e | ||
|
|
886ab250f0 | ||
|
|
58eea06064 | ||
|
|
cd7b9082aa | ||
|
|
fb1cc07c08 | ||
|
|
482a140ce7 | ||
|
|
0123ab82cc | ||
|
|
3caafba34f | ||
|
|
07339fa78f | ||
|
|
637be3d60c | ||
|
|
6868874c98 | ||
|
|
0932e7d9a8 | ||
|
|
b53372e86a | ||
|
|
b1f2c1dc06 | ||
|
|
d6bc11318d | ||
|
|
eabc65846b | ||
|
|
b873f57cd0 | ||
|
|
c8a849c2f9 | ||
|
|
7fc2dff9bc | ||
|
|
ca5a266c81 | ||
|
|
39f182cbaa | ||
|
|
0684ae0cc1 | ||
|
|
b7dc94c65f | ||
|
|
12566dd80d | ||
|
|
4167943209 | ||
|
|
9dcf8afb5d | ||
|
|
6fdced88c5 | ||
|
|
0a563b177a | ||
|
|
4a63305d74 | ||
|
|
87ff477cb6 | ||
|
|
101ffc09eb | ||
|
|
32ff1e7281 | ||
|
|
d146e672a6 | ||
|
|
ac976d9edb | ||
|
|
48baf6c322 | ||
|
|
47a06aa077 | ||
|
|
1bf3eb68ce | ||
|
|
60dffc9412 | ||
|
|
b0a6803f9b | ||
|
|
f90c2894a8 | ||
|
|
ed4de7d484 | ||
|
|
74b7f5cf27 | ||
|
|
824b68911b | ||
|
|
dbad17f16f | ||
|
|
eb8fab8023 | ||
|
|
7b322e4dc2 | ||
|
|
676ca0067b | ||
|
|
e8bb7f7c81 | ||
|
|
3ee4c06af6 | ||
|
|
ab989d8316 | ||
|
|
d306b9d600 | ||
|
|
0ffb4c4e0b | ||
|
|
e914d34ac3 | ||
|
|
a6538bea40 | ||
|
|
441acb383d | ||
|
|
ed68c49095 | ||
|
|
f4efc1b596 | ||
|
|
2747e2e3bf | ||
|
|
329cdf1a9d | ||
|
|
0c7e2e7203 | ||
|
|
dfc4efb1e2 | ||
|
|
dba867594c | ||
|
|
d9aefe565a | ||
|
|
9182b351c1 | ||
|
|
0ae55a3e6c | ||
|
|
029a4b21b3 | ||
|
|
e596fea454 | ||
|
|
0c72f456aa | ||
|
|
26f2736a2b | ||
|
|
ac4c4dfabd | ||
|
|
1245054535 | ||
|
|
f72fb0caf0 | ||
|
|
e4af2bf0d3 | ||
|
|
49a1af6aaa | ||
|
|
c2be9ccfc3 | ||
|
|
0da6a5121f | ||
|
|
6b3ca8fc04 | ||
|
|
f47603c290 | ||
|
|
9709c8fe08 | ||
|
|
af45a23fa9 | ||
|
|
bc97f5ac19 | ||
|
|
6d71967120 | ||
|
|
9b51892b80 |
@@ -1,34 +1,37 @@
|
||||
<!--
|
||||
Make sure you've read and understood our contributing guidelines;
|
||||
Please make sure you've read and understood our contributing guidelines;
|
||||
https://github.com/docker/cli/blob/master/CONTRIBUTING.md
|
||||
|
||||
** Make sure all your commits include a signature generated with `git commit -s` **
|
||||
Make sure commits are signed off (`git commit -s`) with your real name.
|
||||
|
||||
If this is a bug fix, make sure your description includes "fixes #xxxx", or
|
||||
"closes #xxxx"
|
||||
If the PR relates to an existing issue or PR, mention it at the top:
|
||||
|
||||
Provide the following information:
|
||||
- Fixes: https://github.com/docker/cli/issues/12345678
|
||||
- Replaces: https://github.com/docker/cli/pull/12345678
|
||||
- Follow up to: https://github.com/docker/cli/pull/12345678
|
||||
- Related to: https://github.com/docker/cli/issues/12345678
|
||||
-->
|
||||
|
||||
**- What I did**
|
||||
## Summary
|
||||
|
||||
**- How I did it**
|
||||
|
||||
**- How to verify it**
|
||||
|
||||
**- Human readable description for the release notes**
|
||||
## Release notes (optional)
|
||||
|
||||
<!--
|
||||
Write a short (one line) summary that describes the changes in this
|
||||
pull request for inclusion in the changelog.
|
||||
It must be placed inside the below triple backticks section.
|
||||
One-sentence user-facing release note.
|
||||
Leave empty if the change is not changelog-worthy.
|
||||
Use present tense and imperative tone.
|
||||
|
||||
NOTE: Only fill this section if changes introduced in this PR are user-facing.
|
||||
The PR must have a relevant impact/ label.
|
||||
Good:
|
||||
- Fix a panic when running `docker top` on a non-running Windows container.
|
||||
- Don't print warnings in `docker info` for broken symlinks in CLI plugin directories.
|
||||
|
||||
Bad:
|
||||
- Refactor test TestFooWithBar
|
||||
- Fixed a panic when running docker top
|
||||
-->
|
||||
|
||||
```markdown changelog
|
||||
|
||||
```
|
||||
|
||||
**- A picture of a cute animal (not mandatory but encouraged)**
|
||||
## A picture of a cute animal (not mandatory but encouraged)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"$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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -29,7 +29,7 @@ on:
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-26.04
|
||||
outputs:
|
||||
matrix: ${{ steps.platforms.outputs.matrix }}
|
||||
steps:
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
echo ${{ steps.platforms.outputs.matrix }}
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-26.04
|
||||
needs:
|
||||
- prepare
|
||||
strategy:
|
||||
@@ -65,10 +65,10 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
|
||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
set: |
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
-
|
||||
name: Upload artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
path: /tmp/out/*
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
connection_id: ${{ vars.DOCKERHUB_OIDC_CONNECTIONID }}
|
||||
|
||||
prepare-plugins:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-26.04
|
||||
outputs:
|
||||
matrix: ${{ steps.platforms.outputs.matrix }}
|
||||
steps:
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
echo ${{ steps.platforms.outputs.matrix }}
|
||||
|
||||
plugins:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-26.04
|
||||
needs:
|
||||
- prepare-plugins
|
||||
strategy:
|
||||
@@ -156,10 +156,10 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
|
||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
||||
with:
|
||||
targets: plugins-cross
|
||||
set: |
|
||||
|
||||
@@ -34,7 +34,7 @@ on:
|
||||
|
||||
jobs:
|
||||
codeql:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
DISABLE_WARN_OUTSIDE_CONTAINER: '1'
|
||||
@@ -64,18 +64,18 @@ jobs:
|
||||
name: Update Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: "1.26.8"
|
||||
go-version: "1.26.10"
|
||||
cache: false
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
||||
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
with:
|
||||
languages: go
|
||||
-
|
||||
name: Autobuild
|
||||
uses: github/codeql-action/autobuild@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
||||
uses: github/codeql-action/autobuild@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
-
|
||||
name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
||||
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
with:
|
||||
category: "/language:go"
|
||||
|
||||
@@ -26,7 +26,7 @@ on:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-26.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -37,6 +37,7 @@ jobs:
|
||||
- alpine
|
||||
- debian
|
||||
engine-version:
|
||||
- master # moby/moby-bin:master
|
||||
- rc # latest rc
|
||||
- 29 # latest
|
||||
- 28 # latest - 1
|
||||
@@ -51,7 +52,7 @@ jobs:
|
||||
name: Update daemon.json
|
||||
run: |
|
||||
if [ ! -f /etc/docker/daemon.json ]; then
|
||||
# ubuntu 24.04 runners no longer have a default daemon.json present
|
||||
# ubuntu runners no longer have a default daemon.json present
|
||||
sudo mkdir -p /etc/docker/
|
||||
echo '{"experimental": true}' | sudo tee /etc/docker/daemon.json
|
||||
else
|
||||
@@ -65,7 +66,7 @@ jobs:
|
||||
docker info
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
-
|
||||
name: Run ${{ matrix.target }}
|
||||
run: |
|
||||
@@ -76,7 +77,7 @@ jobs:
|
||||
TESTFLAGS: -coverprofile=/tmp/coverage/coverage.txt
|
||||
-
|
||||
name: Send to Codecov
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
|
||||
with:
|
||||
files: ./build/coverage/coverage.txt
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload context
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: pr-review-context
|
||||
path: context/
|
||||
|
||||
@@ -11,7 +11,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
review:
|
||||
uses: docker/docker-agent-action/.github/workflows/review-pr.yml@06e1767af06263c93d712449cbf859778d9392ee # v2.0.5
|
||||
uses: docker/docker-agent-action/.github/workflows/review-pr.yml@3654ec391b070e247de2e2e3c1ff2519b6378b3a # v2.0.9
|
||||
permissions:
|
||||
contents: read # Read repository files and PR diffs
|
||||
pull-requests: write # Post review comments
|
||||
|
||||
@@ -26,7 +26,7 @@ on:
|
||||
|
||||
jobs:
|
||||
sync-release-branch:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-26.04
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
push-release-branch:
|
||||
needs: sync-release-branch
|
||||
if: ${{ !inputs.dry_run && needs.sync-release-branch.outputs.has_changes == 'true' }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-26.04
|
||||
environment: docker-releases
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -26,19 +26,19 @@ on:
|
||||
|
||||
jobs:
|
||||
ctn:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
-
|
||||
name: Test
|
||||
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
|
||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
||||
with:
|
||||
targets: test-coverage
|
||||
-
|
||||
name: Send to Codecov
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
|
||||
with:
|
||||
files: ./build/coverage/coverage.txt
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: "1.26.8"
|
||||
go-version: "1.26.10"
|
||||
cache: false
|
||||
-
|
||||
name: Test
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
shell: bash
|
||||
-
|
||||
name: Send to Codecov
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
|
||||
with:
|
||||
files: /tmp/coverage.txt
|
||||
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
|
||||
jobs:
|
||||
validate-milestone:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Validate milestone matches VERSION
|
||||
|
||||
@@ -15,7 +15,7 @@ on:
|
||||
|
||||
jobs:
|
||||
check-labels:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
||||
steps:
|
||||
- name: Missing `area/` label
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
run: exit 0
|
||||
|
||||
check-changelog:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
||||
env:
|
||||
HAS_IMPACT_LABEL: ${{ contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') }}
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
echo "$desc"
|
||||
|
||||
check-pr-branch:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
||||
env:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
|
||||
@@ -26,7 +26,7 @@ on:
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-26.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -38,13 +38,13 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Run
|
||||
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
|
||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
# check that the generated Markdown and the checked-in files match
|
||||
validate-md:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
fi
|
||||
|
||||
validate-make:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-26.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
make -f docker.Makefile ${{ matrix.target }}
|
||||
|
||||
validate-gocompat:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-26.04
|
||||
env:
|
||||
GOPATH: ${{ github.workspace }}
|
||||
GO111MODULE: off
|
||||
@@ -101,7 +101,7 @@ jobs:
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: "1.26.8"
|
||||
go-version: "1.26.10"
|
||||
cache: false
|
||||
-
|
||||
name: Run gocompat check
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ run:
|
||||
# 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
|
||||
go: "1.26.8"
|
||||
go: "1.26.10"
|
||||
|
||||
timeout: 5m
|
||||
|
||||
|
||||
+4
-4
@@ -8,11 +8,11 @@ ARG BASE_VARIANT=alpine
|
||||
ARG ALPINE_VERSION=3.23
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
ARG GO_VERSION=1.26.8
|
||||
ARG GO_VERSION=1.26.10
|
||||
|
||||
# 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.
|
||||
ARG XX_VERSION=1.9.0
|
||||
ARG XX_VERSION=1.10.0
|
||||
|
||||
# GOVERSIONINFO_VERSION is the version of GoVersionInfo to install.
|
||||
# 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.
|
||||
# It must be a tag in the docker.io/docker/buildx-bin image repository
|
||||
# on Docker Hub.
|
||||
ARG BUILDX_VERSION=0.34.1
|
||||
ARG BUILDX_VERSION=0.38.0
|
||||
|
||||
# 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
|
||||
# on Docker Hub.
|
||||
ARG COMPOSE_VERSION=v5.1.3
|
||||
ARG COMPOSE_VERSION=v5.6.0
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
||||
|
||||
@@ -30,7 +30,11 @@ package hooks
|
||||
type ResponseType int
|
||||
|
||||
const (
|
||||
// NextSteps renders suggestions under a shared "What's next:" header.
|
||||
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
|
||||
|
||||
@@ -7,17 +7,42 @@ const (
|
||||
indent = " "
|
||||
)
|
||||
|
||||
// PrintNextSteps renders list of [NextSteps] messages and writes them
|
||||
// to out. It is a no-op if messages is empty.
|
||||
func PrintNextSteps(out io.Writer, messages []string) {
|
||||
if len(messages) == 0 {
|
||||
return
|
||||
// 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
|
||||
}
|
||||
|
||||
_, _ = io.WriteString(out, whatsNext)
|
||||
for _, msg := range messages {
|
||||
_, _ = io.WriteString(out, indent)
|
||||
_, _ = io.WriteString(out, msg)
|
||||
_, _ = io.WriteString(out, "\n")
|
||||
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
|
||||
// to out. It is a no-op if messages is empty.
|
||||
//
|
||||
// Deprecated: use [PrintMessages] instead.
|
||||
func PrintNextSteps(out io.Writer, messages []string) {
|
||||
PrintMessages(out, []Response{{Type: NextSteps}}, func(Response) []string {
|
||||
return messages
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,85 @@ import (
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestPrintHookMessages(t *testing.T) {
|
||||
func TestPrintMessages(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"
|
||||
|
||||
tests := []struct {
|
||||
@@ -34,6 +112,11 @@ func TestPrintHookMessages(t *testing.T) {
|
||||
" Foo\n" +
|
||||
" bar\n",
|
||||
},
|
||||
{
|
||||
doc: "preserve empty and indented lines",
|
||||
messages: []string{"", " Details", ""},
|
||||
expectedOutput: header + " \n Details\n \n",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
@@ -43,7 +44,16 @@ func ParseTemplate(hookTemplate string, cmd *cobra.Command) ([]string, error) {
|
||||
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 strings.SplitN(out, "\n", maxMessages), nil
|
||||
lines := strings.SplitN(out, "\n", maxMessages)
|
||||
|
||||
// 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")
|
||||
|
||||
@@ -33,7 +33,17 @@ func TestParseTemplate(t *testing.T) {
|
||||
{
|
||||
doc: "empty template",
|
||||
template: "",
|
||||
expectedOutput: []string{""},
|
||||
expectedOutput: nil,
|
||||
},
|
||||
{
|
||||
doc: "only empty lines",
|
||||
template: "\n\n",
|
||||
expectedOutput: nil,
|
||||
},
|
||||
{
|
||||
doc: "whitespace-only lines",
|
||||
template: " \n\t\n ",
|
||||
expectedOutput: nil,
|
||||
},
|
||||
{
|
||||
doc: "plain message",
|
||||
|
||||
@@ -49,11 +49,18 @@ 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) {
|
||||
nextSteps := invokeAndCollectHooks(ctx, cfg, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
|
||||
hooks.PrintNextSteps(subCommand.ErrOrStderr(), nextSteps)
|
||||
messages := invokeAndCollectHooks(ctx, cfg, rootCmd, invokedCommand, flags, cmdErrorMessage)
|
||||
hooks.PrintMessages(subCommand.ErrOrStderr(), messages, func(message hooks.Response) []string {
|
||||
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, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
|
||||
func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []hooks.Response {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -64,17 +71,17 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
|
||||
}
|
||||
|
||||
pluginDirs := getPluginDirs(cfg)
|
||||
nextSteps := make([]string, 0, len(pluginsCfg))
|
||||
var collected []hooks.Response
|
||||
|
||||
tryInvokeHook := func(pluginName string, pluginCfg map[string]string) (messages []string, ok bool, err error) {
|
||||
tryInvokeHook := func(pluginName string, pluginCfg map[string]string) (hooks.Response, error) {
|
||||
match, matched := pluginMatch(pluginCfg, subCmdStr, cmdErrorMessage)
|
||||
if !matched {
|
||||
return nil, false, nil
|
||||
return hooks.Response{}, nil
|
||||
}
|
||||
|
||||
p, err := getPlugin(pluginName, pluginDirs, rootCmd)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return hooks.Response{}, err
|
||||
}
|
||||
|
||||
resp, err := p.RunHook(ctx, hooks.Request{
|
||||
@@ -83,29 +90,23 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
|
||||
CommandError: cmdErrorMessage,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return hooks.Response{}, err
|
||||
}
|
||||
|
||||
var message hooks.Response
|
||||
if err := json.Unmarshal(resp, &message); err != nil {
|
||||
return nil, false, fmt.Errorf("failed to unmarshal hook response (%q): %w", string(resp), err)
|
||||
return hooks.Response{}, fmt.Errorf("failed to unmarshal hook response (%q): %w", string(resp), err)
|
||||
}
|
||||
|
||||
// currently the only hook type
|
||||
if message.Type != hooks.NextSteps {
|
||||
return nil, false, errors.New("unexpected hook response type: " + strconv.Itoa(int(message.Type)))
|
||||
if message.Type != hooks.NextSteps && message.Type != hooks.GenericMessage {
|
||||
return hooks.Response{}, errors.New("unexpected hook response type: " + strconv.Itoa(int(message.Type)))
|
||||
}
|
||||
|
||||
messages, err = hooks.ParseTemplate(message.Template, subCmd)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return messages, true, nil
|
||||
return message, nil
|
||||
}
|
||||
|
||||
for pluginName, pluginCfg := range pluginsCfg {
|
||||
messages, ok, err := tryInvokeHook(pluginName, pluginCfg)
|
||||
message, err := tryInvokeHook(pluginName, pluginCfg)
|
||||
if err != nil {
|
||||
// skip misbehaving plugins, but don't halt execution
|
||||
logrus.WithFields(logrus.Fields{
|
||||
@@ -114,38 +115,13 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
|
||||
}).Debug("Plugin hook invocation failed")
|
||||
continue
|
||||
}
|
||||
if !ok {
|
||||
if message.Template == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
collected = append(collected, message)
|
||||
}
|
||||
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
|
||||
return collected
|
||||
}
|
||||
|
||||
// pluginMatch takes a plugin configuration and a string representing the
|
||||
|
||||
@@ -2,6 +2,11 @@ package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
@@ -258,38 +263,6 @@ 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) {
|
||||
cfg := configfile.New("")
|
||||
cfg.Plugins = map[string]map[string]string{
|
||||
@@ -336,7 +309,7 @@ func TestInvokeAndCollectHooksErrorHooksSkippedOnSuccess(t *testing.T) {
|
||||
// On success, error-hooks should not match, so the plugin
|
||||
// binary is never looked up and no results are returned.
|
||||
result := invokeAndCollectHooks(
|
||||
context.Background(), cfg, root, sub,
|
||||
context.Background(), cfg, root,
|
||||
"build", map[string]string{}, "",
|
||||
)
|
||||
assert.Check(t, is.Len(result, 0))
|
||||
@@ -349,7 +322,7 @@ func TestInvokeAndCollectHooksNoPlugins(t *testing.T) {
|
||||
root.AddCommand(sub)
|
||||
|
||||
result := invokeAndCollectHooks(
|
||||
context.Background(), cfg, root, sub,
|
||||
context.Background(), cfg, root,
|
||||
"build", map[string]string{}, "some error",
|
||||
)
|
||||
assert.Check(t, is.Len(result, 0))
|
||||
@@ -368,8 +341,108 @@ func TestInvokeAndCollectHooksCancelledContext(t *testing.T) {
|
||||
cancel() // cancel immediately
|
||||
|
||||
result := invokeAndCollectHooks(
|
||||
ctx, cfg, root, sub,
|
||||
ctx, cfg, root,
|
||||
"build", map[string]string{}, "exit status 1",
|
||||
)
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// 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
|
||||
|
||||
const (
|
||||
@@ -35,4 +38,7 @@ type Metadata struct {
|
||||
URL string `json:",omitempty"`
|
||||
// Hidden hides the plugin in completion and help message output.
|
||||
Hidden bool `json:",omitempty"`
|
||||
// Features declares optional contracts supported by the plugin, keyed by
|
||||
// feature name.
|
||||
Features map[string]any `json:",omitempty"`
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ type DockerCli struct {
|
||||
serverInfo ServerInfo
|
||||
contextStore store.Store
|
||||
currentContext string
|
||||
contextResolver func(*DockerCli) (string, error)
|
||||
init sync.Once
|
||||
initErr error
|
||||
dockerEndpoint docker.Endpoint
|
||||
@@ -208,6 +209,8 @@ func (cli *DockerCli) HooksEnabled() bool {
|
||||
|
||||
// Initialize the dockerCli runs initialization that must happen after command
|
||||
// line flags are parsed.
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) error {
|
||||
for _, o := range ops {
|
||||
if err := o(cli); err != nil {
|
||||
@@ -257,6 +260,16 @@ 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
|
||||
if cli.enableGlobalMeter {
|
||||
cli.createGlobalMeterProvider(cli.baseCtx)
|
||||
|
||||
@@ -84,6 +84,19 @@ 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.
|
||||
func WithAPIClient(c client.APIClient) CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
|
||||
@@ -325,6 +325,64 @@ func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
|
||||
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) {
|
||||
t.Run("disabled by default", func(t *testing.T) {
|
||||
// Make sure we don't depend on any existing ~/.docker/config.json
|
||||
|
||||
@@ -260,8 +260,11 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions)
|
||||
contextDir = tempDir
|
||||
case build.ContextTypeRemote:
|
||||
buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, options.context, options.dockerfileName)
|
||||
if err != nil && options.quiet {
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), progBuff)
|
||||
if err != nil {
|
||||
if options.quiet {
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), progBuff)
|
||||
}
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unable to prepare context: path %q not found", options.context)
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
@@ -172,6 +174,35 @@ RUN echo hello world
|
||||
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 {
|
||||
context *tar.Reader
|
||||
options client.ImageBuildOptions
|
||||
|
||||
@@ -52,6 +52,14 @@ func TestNewHistoryCommandErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewHistoryCommandSuccess(t *testing.T) {
|
||||
// Set to UTC timezone as timestamps in output are
|
||||
// printed in the current timezone
|
||||
local := time.Local
|
||||
time.Local = time.UTC
|
||||
t.Cleanup(func() {
|
||||
time.Local = local
|
||||
})
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
@@ -118,9 +126,6 @@ func TestNewHistoryCommandSuccess(t *testing.T) {
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Set to UTC timezone as timestamps in output are
|
||||
// printed in the current timezone
|
||||
t.Setenv("TZ", "UTC")
|
||||
cli := test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc})
|
||||
cmd := newHistoryCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
|
||||
+10
-10
@@ -5,13 +5,13 @@ go 1.26.0
|
||||
require (
|
||||
github.com/containerd/errdefs v1.0.0
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/docker/cli v29.7.2+incompatible
|
||||
github.com/docker/cli v29.8.2+incompatible
|
||||
github.com/docker/cli-docs-tool v0.11.0
|
||||
github.com/docker/distribution v2.8.3+incompatible
|
||||
github.com/docker/go-connections v0.8.1
|
||||
github.com/docker/go-connections v0.8.2
|
||||
github.com/fvbommel/sortorder v1.2.0
|
||||
github.com/moby/moby/api v1.55.0
|
||||
github.com/moby/moby/client v0.5.1
|
||||
github.com/moby/moby/api v1.56.1
|
||||
github.com/moby/moby/client v0.6.2
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/sirupsen/logrus v1.10.2
|
||||
github.com/spf13/cobra v1.10.2
|
||||
@@ -22,7 +22,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // 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/trace v1.46.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.56.0 // indirect
|
||||
golang.org/x/net v0.58.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/term v0.45.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/net v0.60.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/term v0.46.0 // indirect
|
||||
golang.org/x/text v0.42.0 // 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/grpc v1.83.2 // indirect
|
||||
|
||||
+20
-21
@@ -1,5 +1,5 @@
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 h1:m2xcjRABtG288vnlV3U/PKh/HnPURxZp50RKNUwBxM8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4/go.mod h1:MTUE7ZuZO89FAtAwozGg6AWD83pcdefnEq5gF82MB8s=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
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/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/docker/cli v29.7.2+incompatible h1:dlkwallR8XqfeVnA2ELEhdwvb4lsSwuB4IgsG8Q9cLY=
|
||||
github.com/docker/cli v29.7.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v29.8.2+incompatible h1:2zgdFuoFst2T80oS42vhKGxkd0JRo305eIEKTsxR7pQ=
|
||||
github.com/docker/cli v29.8.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/go.mod h1:ma8BKiisUo8D6W05XEYIh3oa1UbgrZhi1nowyKFJa8Q=
|
||||
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/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M=
|
||||
github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-connections v0.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc=
|
||||
github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
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/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/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/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
|
||||
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw=
|
||||
github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM=
|
||||
github.com/moby/moby/api v1.56.1 h1:PpWkvVPB7Fr/No8w+TfyJ/I6rWZ2YPZhT3JorIg+06c=
|
||||
github.com/moby/moby/api v1.56.1/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.2/go.mod h1:DGcnO7qmPJrqobx/ggQgQ/AUk+2wgVdQv7uxB0/mEsc=
|
||||
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/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
|
||||
@@ -236,27 +236,26 @@ 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-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.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
|
||||
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
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.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
|
||||
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
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-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-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-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
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/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
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/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,682 @@
|
||||
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)
|
||||
}
|
||||
+24
-4
@@ -178,6 +178,9 @@ func newDockerCommand(dockerCli *command.DockerCli) *cli.TopLevelCommand {
|
||||
_ = cmd.RegisterFlagCompletionFunc("log-level", completeLogLevels)
|
||||
|
||||
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)
|
||||
|
||||
setupHelpCommand(dockerCli, cmd, helpCmd)
|
||||
@@ -250,6 +253,7 @@ func setHelpFunc(dockerCli command.Cli, cmd *cobra.Command) {
|
||||
ccmd.Println(err)
|
||||
return
|
||||
}
|
||||
updateCloudFlagVisibility(dockerCli, ccmd.Root())
|
||||
|
||||
if len(args) >= 1 {
|
||||
err := tryRunPluginHelp(dockerCli, ccmd, args)
|
||||
@@ -508,7 +512,18 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tcmd.Initialize(command.WithEnableGlobalMeterProvider(), command.WithEnableGlobalTracerProvider()); err != nil {
|
||||
if err := tcmd.Initialize(
|
||||
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
|
||||
}
|
||||
|
||||
@@ -538,6 +553,7 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
|
||||
if err := pluginmanager.AddPluginCommandStubs(dockerCli, cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
updateCloudFlagVisibility(dockerCli, cmd)
|
||||
}
|
||||
|
||||
var subCommand *cobra.Command
|
||||
@@ -555,11 +571,15 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
|
||||
return nil
|
||||
}
|
||||
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
|
||||
}
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.26.8
|
||||
ARG GO_VERSION=1.26.10
|
||||
|
||||
# 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
|
||||
@@ -10,7 +10,7 @@ ARG ALPINE_VERSION=3.23
|
||||
# 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
|
||||
# on Docker Hub.
|
||||
ARG BUILDX_VERSION=0.34.1
|
||||
ARG BUILDX_VERSION=0.38.0
|
||||
FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS golang
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.26.8
|
||||
ARG GO_VERSION=1.26.10
|
||||
|
||||
# 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.26.8
|
||||
ARG GO_VERSION=1.26.10
|
||||
|
||||
# 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
|
||||
|
||||
@@ -31,6 +31,7 @@ services:
|
||||
dockerfile: engine/Dockerfile
|
||||
args:
|
||||
ENGINE_VERSION: ${ENGINE_VERSION:-29}
|
||||
ENGINE_SOURCE: ${ENGINE_SOURCE:-dind}
|
||||
privileged: true
|
||||
command: ['--insecure-registry=registry:5000', '--insecure-registry=privateregistry:5001', '--experimental']
|
||||
environment:
|
||||
|
||||
@@ -112,7 +112,7 @@ func TestRunWithCgroupNamespace(t *testing.T) {
|
||||
|
||||
func TestRunUmask(t *testing.T) {
|
||||
environment.SkipIfDaemonNotLinux(t)
|
||||
t.Skip("FIXME: un-skip once e2e tests v29.8.0")
|
||||
skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.56"))
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
+9
-1
@@ -3,8 +3,16 @@
|
||||
# ENGINE_VERSION is the version of the (docker-in-docker) Docker Engine to
|
||||
# test against.
|
||||
ARG ENGINE_VERSION=29
|
||||
# ENGINE_SOURCE is "dind" or "moby-bin" (moby/moby-bin:master).
|
||||
ARG ENGINE_SOURCE=dind
|
||||
|
||||
FROM docker:${ENGINE_VERSION}-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}
|
||||
|
||||
# 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 && \
|
||||
|
||||
Vendored
+10
-1
@@ -1,5 +1,14 @@
|
||||
ARG ENGINE_VERSION
|
||||
FROM docker:${ENGINE_VERSION}-dind
|
||||
# ENGINE_SOURCE is "dind" or "moby-bin" (moby/moby-bin:master).
|
||||
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.
|
||||
COPY registry/certs/ca.crt /usr/local/share/ca-certificates/tlsregistry-ca.crt
|
||||
|
||||
@@ -42,6 +42,11 @@ setup() {
|
||||
sh e2e/testdata/registry/certs/gen-certs.sh
|
||||
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
|
||||
|
||||
# Ensure supporting services exist before running tests. If one fails to start,
|
||||
|
||||
+19
-19
@@ -4,7 +4,7 @@ module github.com/docker/cli
|
||||
// There is no 'go.mod' file, as that would imply opting in for all the rules
|
||||
// around SemVer, which this repo cannot abide by as it uses CalVer.
|
||||
|
||||
go 1.26
|
||||
go 1.26.0
|
||||
|
||||
tool (
|
||||
github.com/cpuguy83/go-md2man/v2 // for scripts/docs/generate-man.sh
|
||||
@@ -15,14 +15,14 @@ require (
|
||||
dario.cat/mergo v1.0.2
|
||||
github.com/containerd/errdefs v1.0.0
|
||||
github.com/containerd/errdefs/pkg v0.3.0
|
||||
github.com/containerd/log v0.1.0
|
||||
github.com/containerd/log v0.2.0
|
||||
github.com/containerd/platforms v1.0.0-rc.5
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/docker/cli-docs-tool v0.11.0
|
||||
github.com/docker/distribution v2.8.3+incompatible
|
||||
github.com/docker/docker-credential-helpers v0.9.9
|
||||
github.com/docker/go-connections v0.8.1
|
||||
github.com/docker/go-connections v0.8.2
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/fvbommel/sortorder v1.2.0
|
||||
github.com/go-jose/go-jose/v4 v4.1.5
|
||||
@@ -31,10 +31,10 @@ require (
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mattn/go-runewidth v0.0.28
|
||||
github.com/mattn/go-runewidth v0.0.31
|
||||
github.com/moby/go-archive v0.3.3
|
||||
github.com/moby/moby/api v1.56.0
|
||||
github.com/moby/moby/client v0.6.0
|
||||
github.com/moby/moby/api v1.56.1
|
||||
github.com/moby/moby/client v0.6.2
|
||||
github.com/moby/patternmatcher v0.6.1
|
||||
github.com/moby/swarmkit/v2 v2.1.2
|
||||
github.com/moby/sys/atomicwriter v0.1.0
|
||||
@@ -61,21 +61,21 @@ require (
|
||||
go.opentelemetry.io/otel/sdk/metric v1.46.0
|
||||
go.opentelemetry.io/otel/trace v1.46.0
|
||||
go.yaml.in/yaml/v3 v3.0.5
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/term v0.45.0
|
||||
golang.org/x/text v0.41.0
|
||||
golang.org/x/sync v0.23.0
|
||||
golang.org/x/sys v0.48.0
|
||||
golang.org/x/term v0.46.0
|
||||
golang.org/x/text v0.42.0
|
||||
gotest.tools/v3 v3.5.2
|
||||
tags.cncf.io/container-device-interface v1.1.0
|
||||
tags.cncf.io/container-device-interface v1.1.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.3 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/docker/go-events v0.1.0 // indirect
|
||||
github.com/docker/go-metrics v0.1.0 // indirect
|
||||
@@ -87,10 +87,10 @@ require (
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.19.2 // indirect
|
||||
github.com/klauspost/compress v1.20.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/userns v0.2.0 // indirect
|
||||
github.com/moby/sys/userns v0.2.1 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/prometheus/client_golang v1.22.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
@@ -101,9 +101,9 @@ require (
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
|
||||
golang.org/x/mod v0.40.0 // indirect
|
||||
golang.org/x/net v0.58.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/mod v0.41.0 // indirect
|
||||
golang.org/x/net v0.60.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/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
|
||||
google.golang.org/grpc v1.83.2 // indirect
|
||||
|
||||
+36
-37
@@ -2,24 +2,24 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
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/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 h1:m2xcjRABtG288vnlV3U/PKh/HnPURxZp50RKNUwBxM8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4/go.mod h1:MTUE7ZuZO89FAtAwozGg6AWD83pcdefnEq5gF82MB8s=
|
||||
github.com/Microsoft/go-winio v0.6.3 h1:M/52tsoyTfwaxajLnXZm7PqkbMGRH8iSWP+8fcg9MQ4=
|
||||
github.com/Microsoft/go-winio v0.6.3/go.mod h1:ma0QpxizD4fyyzJ1C7MIblymTD2Nxz5Mho/3CepwIZg=
|
||||
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/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
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/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/log v0.2.0 h1:BewD/umNgVnoczglOpX8eRMyEy5t5iPlu5AIpnWDONc=
|
||||
github.com/containerd/log v0.2.0/go.mod h1:/M7L7CXKcPTfNC74XzaK+5H5KbO5+4lJVpuVI6vRLoM=
|
||||
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/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/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/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M=
|
||||
github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-connections v0.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc=
|
||||
github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
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-metrics v0.1.0 h1:r76KPNpstz+IvQKSWpYegSkkyzex0V3A1ZGVx6bhGlY=
|
||||
@@ -78,20 +78,20 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
||||
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/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
|
||||
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
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/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU=
|
||||
github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
|
||||
github.com/mattn/go-runewidth v0.0.31 h1:1oCV4ReTlosDBc6rYRw5cL8S/4y8iv5oRl4SJ5IBgwo=
|
||||
github.com/mattn/go-runewidth v0.0.31/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/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/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
|
||||
github.com/moby/moby/api v1.56.0 h1:GQzua3NA599ASSIICx0iFgiJeO9YkdDARvQsm23ZZuQ=
|
||||
github.com/moby/moby/api v1.56.0/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk=
|
||||
github.com/moby/moby/client v0.6.0 h1:AJjEB21QPbXSXjDsZorFBoDZPhMrfbpaPLgSMAW9Bgs=
|
||||
github.com/moby/moby/client v0.6.0/go.mod h1:OCo00wNRyA3m4lmJ228W3JbyCN4ZNNYjpOXiJydBdcQ=
|
||||
github.com/moby/moby/api v1.56.1 h1:PpWkvVPB7Fr/No8w+TfyJ/I6rWZ2YPZhT3JorIg+06c=
|
||||
github.com/moby/moby/api v1.56.1/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.2/go.mod h1:DGcnO7qmPJrqobx/ggQgQ/AUk+2wgVdQv7uxB0/mEsc=
|
||||
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
|
||||
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/swarmkit/v2 v2.1.2 h1:1WDZAI6HVYNKdCG4zlXnTAPyLsLwuhRGWlHoOUf5Z6I=
|
||||
@@ -112,8 +112,8 @@ github.com/moby/sys/symlink v0.3.0 h1:GZX89mEZ9u53f97npBy4Rc3vJKj7JBDj/PN2I22GrN
|
||||
github.com/moby/sys/symlink v0.3.0/go.mod h1:3eNdhduHmYPcgsJtZXW1W4XUJdZGBIkttZ8xKqPUJq0=
|
||||
github.com/moby/sys/user v0.4.1 h1:RgjRlaDKi/Xmyrz4t8lyzXT6v2ooFeO/7xtchmhVWE0=
|
||||
github.com/moby/sys/user v0.4.1/go.mod h1:E9QsW5WRe1kUAf7kW8hXKwu1uhsZEAdPLYHYSDudF4Y=
|
||||
github.com/moby/sys/userns v0.2.0 h1:nEtDtp7NCV/6dutSklNe8FrENPwFdc4mXnZqC/JWgXM=
|
||||
github.com/moby/sys/userns v0.2.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/sys/userns v0.2.1 h1:4OvdM7BcPkASbuouHsbW3aeMJSFlYDldBRnXVZhaRk8=
|
||||
github.com/moby/sys/userns v0.2.1/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
|
||||
@@ -187,34 +187,33 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
|
||||
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
|
||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
|
||||
golang.org/x/net v0.60.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-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.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
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-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-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
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/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
|
||||
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
@@ -238,5 +237,5 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||
tags.cncf.io/container-device-interface v1.1.0 h1:RnxNhxF1JOu6CJUVpetTYvrXHdxw9j9jFYgZpI+anSY=
|
||||
tags.cncf.io/container-device-interface v1.1.0/go.mod h1:76Oj0Yqp9FwTx/pySDc8Bxjpg+VqXfDb50cKAXVJ34Q=
|
||||
tags.cncf.io/container-device-interface v1.1.1 h1:YPwQz4xg8PlQ0yT/baR0BtLpTQROe4l6M1yuRgAu1vc=
|
||||
tags.cncf.io/container-device-interface v1.1.1/go.mod h1:S1PSJWYPD4Iom0/39mvr/VVFCfG0Yt14j20d0OYrY2M=
|
||||
|
||||
+3
-3
@@ -26,7 +26,7 @@ func (csiState csiEntryState) Handle(b byte) (s state, e error) {
|
||||
|
||||
func (csiState csiEntryState) Transition(s state) error {
|
||||
csiState.parser.logf("CsiEntry::Transition %s --> %s", csiState.Name(), s.Name())
|
||||
csiState.baseState.Transition(s)
|
||||
_ = csiState.baseState.Transition(s)
|
||||
|
||||
switch s {
|
||||
case csiState.parser.ground:
|
||||
@@ -34,9 +34,9 @@ func (csiState csiEntryState) Transition(s state) error {
|
||||
case csiState.parser.csiParam:
|
||||
switch {
|
||||
case sliceContains(csiParams, csiState.parser.context.currentChar):
|
||||
csiState.parser.collectParam()
|
||||
_ = csiState.parser.collectParam()
|
||||
case sliceContains(intermeds, csiState.parser.context.currentChar):
|
||||
csiState.parser.collectInter()
|
||||
_ = csiState.parser.collectInter()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-3
@@ -16,8 +16,7 @@ func (csiState csiParamState) Handle(b byte) (s state, e error) {
|
||||
case sliceContains(alphabetics, b):
|
||||
return csiState.parser.ground, nil
|
||||
case sliceContains(csiCollectables, b):
|
||||
csiState.parser.collectParam()
|
||||
return csiState, nil
|
||||
return csiState, csiState.parser.collectParam()
|
||||
case sliceContains(executors, b):
|
||||
return csiState, csiState.parser.execute()
|
||||
}
|
||||
@@ -27,7 +26,7 @@ func (csiState csiParamState) Handle(b byte) (s state, e error) {
|
||||
|
||||
func (csiState csiParamState) Transition(s state) error {
|
||||
csiState.parser.logf("CsiParam::Transition %s --> %s", csiState.Name(), s.Name())
|
||||
csiState.baseState.Transition(s)
|
||||
_ = csiState.baseState.Transition(s)
|
||||
|
||||
switch s {
|
||||
case csiState.parser.ground:
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ func (escState escapeIntermediateState) Handle(b byte) (s state, e error) {
|
||||
|
||||
func (escState escapeIntermediateState) Transition(s state) error {
|
||||
escState.parser.logf("escapeIntermediateState::Transition %s --> %s", escState.Name(), s.Name())
|
||||
escState.baseState.Transition(s)
|
||||
_ = escState.baseState.Transition(s)
|
||||
|
||||
switch s {
|
||||
case escState.parser.ground:
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ func (escState escapeState) Handle(b byte) (s state, e error) {
|
||||
|
||||
func (escState escapeState) Transition(s state) error {
|
||||
escState.parser.logf("Escape::Transition %s --> %s", escState.Name(), s.Name())
|
||||
escState.baseState.Transition(s)
|
||||
_ = escState.baseState.Transition(s)
|
||||
|
||||
switch s {
|
||||
case escState.parser.ground:
|
||||
|
||||
+5
-8
@@ -113,16 +113,13 @@ func (ap *AnsiParser) handle(b byte) error {
|
||||
|
||||
if newState == 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 err := ap.changeState(newState); err != nil {
|
||||
return err
|
||||
}
|
||||
if newState == ap.currState {
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
return ap.changeState(newState)
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) changeState(newState state) error {
|
||||
@@ -136,7 +133,7 @@ func (ap *AnsiParser) changeState(newState state) error {
|
||||
|
||||
// Perform transition action
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
+5
-9
@@ -4,9 +4,9 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func parseParams(bytes []byte) ([]string, error) {
|
||||
paramBuff := make([]byte, 0, 0)
|
||||
params := []string{}
|
||||
func parseParams(bytes []byte) []string {
|
||||
var paramBuff []byte
|
||||
var params []string
|
||||
|
||||
for _, v := range bytes {
|
||||
if v == ';' {
|
||||
@@ -14,7 +14,7 @@ func parseParams(bytes []byte) ([]string, error) {
|
||||
// Completed parameter, append it to the list
|
||||
s := string(paramBuff)
|
||||
params = append(params, s)
|
||||
paramBuff = make([]byte, 0, 0)
|
||||
paramBuff = paramBuff[:0]
|
||||
}
|
||||
} else {
|
||||
paramBuff = append(paramBuff, v)
|
||||
@@ -27,11 +27,7 @@ func parseParams(bytes []byte) ([]string, error) {
|
||||
params = append(params, s)
|
||||
}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func parseCmd(context ansiContext) (string, error) {
|
||||
return string(context.currentChar), nil
|
||||
return params
|
||||
}
|
||||
|
||||
func getInt(params []string, dflt int) int {
|
||||
|
||||
+7
-9
@@ -15,10 +15,9 @@ func (ap *AnsiParser) collectInter() error {
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) escDispatch() error {
|
||||
cmd, _ := parseCmd(*ap.context)
|
||||
intermeds := ap.context.interBuffer
|
||||
cmd := string(ap.context.currentChar)
|
||||
ap.logf("escDispatch currentChar: %#x", ap.context.currentChar)
|
||||
ap.logf("escDispatch: %v(%v)", cmd, intermeds)
|
||||
ap.logf("escDispatch: %s(%q)", cmd, string(ap.context.interBuffer))
|
||||
|
||||
switch cmd {
|
||||
case "D": // IND
|
||||
@@ -31,14 +30,14 @@ func (ap *AnsiParser) escDispatch() error {
|
||||
return err
|
||||
case "M": // RI
|
||||
return ap.eventHandler.RI()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) csiDispatch() error {
|
||||
cmd, _ := parseCmd(*ap.context)
|
||||
params, _ := parseParams(ap.context.paramBuffer)
|
||||
cmd := string(ap.context.currentChar)
|
||||
params := parseParams(ap.context.paramBuffer)
|
||||
ap.logf("Parsed params: %v with length: %d", params, len(params))
|
||||
|
||||
ap.logf("csiDispatch: %v(%v)", cmd, params)
|
||||
@@ -109,9 +108,8 @@ func (ap *AnsiParser) print() error {
|
||||
return ap.eventHandler.Print(ap.context.currentChar)
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) clear() error {
|
||||
func (ap *AnsiParser) clear() {
|
||||
ap.context = &ansiContext{}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) execute() error {
|
||||
|
||||
-2
@@ -1,7 +1,5 @@
|
||||
package ansiterm
|
||||
|
||||
type stateID int
|
||||
|
||||
type state interface {
|
||||
Enter() error
|
||||
Exit() error
|
||||
|
||||
-10
@@ -1,9 +1,5 @@
|
||||
package ansiterm
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func sliceContains(bytes []byte, b byte) bool {
|
||||
for _, v := range bytes {
|
||||
if v == b {
|
||||
@@ -13,9 +9,3 @@ func sliceContains(bytes []byte, b byte) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func convertBytesToInteger(bytes []byte) int {
|
||||
s := string(bytes)
|
||||
i, _ := strconv.Atoi(s)
|
||||
return i
|
||||
}
|
||||
|
||||
+1
-98
@@ -5,12 +5,9 @@ package winterm
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/Azure/go-ansiterm"
|
||||
windows "golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// Windows keyboard constants
|
||||
@@ -55,100 +52,6 @@ const (
|
||||
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
|
||||
// the passed min / max range.
|
||||
func ensureInRange(n int16, min int16, max int16) int16 {
|
||||
|
||||
+153
-127
@@ -1,9 +1,15 @@
|
||||
version: "2"
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt # files are gofmt'ed
|
||||
linters:
|
||||
enable:
|
||||
# style
|
||||
- containedctx # struct contains a context
|
||||
- dupl # duplicate code
|
||||
- errname # erorrs are named correctly
|
||||
- dupword # duplicate words in comments
|
||||
- errname # errors are named correctly
|
||||
- intrange # integer ranges in for loop
|
||||
- nolintlint # "//nolint" directives are properly explained
|
||||
- revive # golint replacement
|
||||
- unconvert # unnecessary conversions
|
||||
@@ -13,135 +19,155 @@ linters:
|
||||
- contextcheck # function uses a non-inherited context
|
||||
- errorlint # errors not wrapped for 1.13
|
||||
- exhaustive # check exhaustiveness of enum switch statements
|
||||
- gofmt # files are gofmt'ed
|
||||
- gocritic # various code quality checks
|
||||
- gosec # security
|
||||
- mirror # avoid allocations
|
||||
- nilerr # returns nil even with non-nil error
|
||||
- thelper # test helpers without t.Helper()
|
||||
- unparam # unused function params
|
||||
|
||||
issues:
|
||||
exclude-dirs:
|
||||
- pkg/etw/sample
|
||||
|
||||
exclude-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
|
||||
- linters:
|
||||
- revive
|
||||
text: "^line-length-limit: "
|
||||
source: "^//(go:generate|sys) "
|
||||
|
||||
#TODO: remove after upgrading to go1.18
|
||||
# ignore comment spacing for nolint and sys directives
|
||||
- linters:
|
||||
- revive
|
||||
text: "^comment-spacings: no space between comment delimiter and comment text"
|
||||
source: "//(cspell:|nolint:|sys |todo)"
|
||||
|
||||
# not on go 1.18 yet, so no any
|
||||
- linters:
|
||||
- revive
|
||||
text: "^use-any: since GO 1.18 'interface{}' can be replaced by 'any'"
|
||||
|
||||
# allow unjustified ignores of error checks in defer statements
|
||||
- linters:
|
||||
- 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'
|
||||
|
||||
|
||||
linters-settings:
|
||||
exhaustive:
|
||||
default-signifies-exhaustive: true
|
||||
govet:
|
||||
enable-all: true
|
||||
disable:
|
||||
# struct order is often for Win32 compat
|
||||
# also, ignore pointer bytes/GC issues for now until performance becomes an issue
|
||||
- fieldalignment
|
||||
nolintlint:
|
||||
require-explanation: true
|
||||
require-specific: 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
|
||||
exclusions:
|
||||
paths:
|
||||
- pkg/etw/sample
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
# rules with required arguments
|
||||
- name: argument-limit
|
||||
disabled: true
|
||||
- name: banned-characters
|
||||
disabled: true
|
||||
- name: cognitive-complexity
|
||||
disabled: true
|
||||
- name: cyclomatic
|
||||
disabled: true
|
||||
- name: file-header
|
||||
disabled: true
|
||||
- name: function-length
|
||||
disabled: true
|
||||
- name: function-result-limit
|
||||
disabled: true
|
||||
- name: max-public-structs
|
||||
disabled: true
|
||||
# geneally annoying rules
|
||||
- name: add-constant # complains about any and all strings and integers
|
||||
disabled: true
|
||||
- name: confusing-naming # we frequently use "Foo()" and "foo()" together
|
||||
disabled: true
|
||||
- name: flag-parameter # excessive, and a common idiom we use
|
||||
disabled: true
|
||||
- name: unhandled-error # warns over common fmt.Print* and io.Close; rely on errcheck instead
|
||||
disabled: true
|
||||
# general config
|
||||
- name: line-length-limit
|
||||
arguments:
|
||||
- 140
|
||||
- name: var-naming
|
||||
arguments:
|
||||
- []
|
||||
- - CID
|
||||
- CRI
|
||||
- CTRD
|
||||
- DACL
|
||||
- DLL
|
||||
- DOS
|
||||
- ETW
|
||||
- FSCTL
|
||||
- GCS
|
||||
- GMSA
|
||||
- HCS
|
||||
- HV
|
||||
- IO
|
||||
- LCOW
|
||||
- LDAP
|
||||
- LPAC
|
||||
- LTSC
|
||||
- MMIO
|
||||
- NT
|
||||
- OCI
|
||||
- PMEM
|
||||
- PWSH
|
||||
- RX
|
||||
- SACl
|
||||
- SID
|
||||
- SMB
|
||||
- TX
|
||||
- VHD
|
||||
- VHDX
|
||||
- VMID
|
||||
- VPCI
|
||||
- WCOW
|
||||
- WIM
|
||||
# err is very often shadowed in nested scopes
|
||||
- linters:
|
||||
- govet
|
||||
text: '^shadow: declaration of "err" shadows declaration'
|
||||
|
||||
# ignore long lines for skip autogen directives
|
||||
- linters:
|
||||
- revive
|
||||
text: "^line-length-limit: "
|
||||
source: "^//(go:generate|sys) "
|
||||
|
||||
# ignore comment spacing for sys directives
|
||||
- linters:
|
||||
- revive
|
||||
text: "^comment-spacings: no space between comment delimiter and comment text"
|
||||
source: "//(cspell:|sys )"
|
||||
|
||||
# allow unjustified ignores of error checks in defer statements
|
||||
- linters:
|
||||
- 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'
|
||||
|
||||
- linters:
|
||||
- gosec
|
||||
text: "^G115: integer overflow conversion"
|
||||
|
||||
|
||||
settings:
|
||||
exhaustive:
|
||||
default-signifies-exhaustive: true
|
||||
gocritic:
|
||||
disable-all: true
|
||||
enabled-checks:
|
||||
- nilValReturn
|
||||
- sprintfQuotedString
|
||||
govet:
|
||||
enable-all: true
|
||||
disable:
|
||||
# struct order is often for Win32 compat
|
||||
# also, ignore pointer bytes/GC issues for now until performance becomes an issue
|
||||
- fieldalignment
|
||||
nolintlint:
|
||||
require-explanation: true
|
||||
require-specific: 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
|
||||
rules:
|
||||
# rules with required arguments
|
||||
- name: argument-limit
|
||||
disabled: true
|
||||
- name: banned-characters
|
||||
disabled: true
|
||||
- name: cognitive-complexity
|
||||
disabled: true
|
||||
- name: cyclomatic
|
||||
disabled: true
|
||||
# covered by "exhaustive" linter
|
||||
- name: enforce-switch-style
|
||||
disabled: true
|
||||
- name: file-header
|
||||
disabled: true
|
||||
- name: function-length
|
||||
disabled: true
|
||||
- name: function-result-limit
|
||||
disabled: true
|
||||
- name: max-public-structs
|
||||
disabled: true
|
||||
# generally annoying rules
|
||||
- name: add-constant # complains about any and all strings and integers
|
||||
disabled: true
|
||||
- name: confusing-naming # we frequently use "Foo()" and "foo()" together
|
||||
disabled: true
|
||||
- name: flag-parameter # excessive, and a common idiom we use
|
||||
disabled: true
|
||||
- name: multiline-if-init # too opinionated; see https://github.com/microsoft/go-winio/pull/325#issuecomment-5822487436
|
||||
disabled: true
|
||||
- name: unhandled-error # warns over common fmt.Print* and io.Close; rely on errcheck instead
|
||||
disabled: true
|
||||
# general config
|
||||
- name: line-length-limit
|
||||
arguments:
|
||||
- 140
|
||||
- name: var-naming
|
||||
arguments:
|
||||
- []
|
||||
- - CID
|
||||
- CRI
|
||||
- CTRD
|
||||
- DACL
|
||||
- DLL
|
||||
- DOS
|
||||
- ETW
|
||||
- FSCTL
|
||||
- GCS
|
||||
- GMSA
|
||||
- HCS
|
||||
- HV
|
||||
- IO
|
||||
- LCOW
|
||||
- LDAP
|
||||
- LPAC
|
||||
- LTSC
|
||||
- MMIO
|
||||
- NT
|
||||
- OCI
|
||||
- PMEM
|
||||
- PWSH
|
||||
- RX
|
||||
- SACl
|
||||
- SID
|
||||
- SMB
|
||||
- TX
|
||||
- VHD
|
||||
- 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
|
||||
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
@@ -36,7 +35,7 @@ const (
|
||||
StreamSparseAttributes = uint32(8)
|
||||
)
|
||||
|
||||
//nolint:revive // var-naming: ALL_CAPS
|
||||
//nolint:revive,nolintlint // var-naming: ALL_CAPS
|
||||
const (
|
||||
WRITE_DAC = windows.WRITE_DAC
|
||||
WRITE_OWNER = windows.WRITE_OWNER
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ func writeEa(buf *bytes.Buffer, ea *ExtendedAttribute, last bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = buf.Write([]byte(ea.Name))
|
||||
_, err = buf.WriteString(ea.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+34
-12
@@ -1,11 +1,11 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -21,16 +21,33 @@ import (
|
||||
//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
|
||||
|
||||
var (
|
||||
ErrFileClosed = errors.New("file has already been closed")
|
||||
ErrTimeout = &timeoutError{}
|
||||
)
|
||||
// ErrFileClosed is returned when an operation is performed on a closed file.
|
||||
//
|
||||
// Deprecated: use [os.ErrClosed] instead.
|
||||
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{}
|
||||
|
||||
func (*timeoutError) Error() string { return "i/o timeout" }
|
||||
func (*timeoutError) Timeout() 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{}
|
||||
|
||||
@@ -144,7 +161,7 @@ func (f *win32File) prepareIO() (*ioOperation, error) {
|
||||
f.wgLock.RLock()
|
||||
if f.closing.Load() {
|
||||
f.wgLock.RUnlock()
|
||||
return nil, ErrFileClosed
|
||||
return nil, os.ErrClosed
|
||||
}
|
||||
f.wg.Add(1)
|
||||
f.wgLock.RUnlock()
|
||||
@@ -193,7 +210,7 @@ func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, er
|
||||
err = r.err
|
||||
if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
|
||||
if f.closing.Load() {
|
||||
err = ErrFileClosed
|
||||
err = os.ErrClosed
|
||||
}
|
||||
} else if err != nil && f.socket {
|
||||
// err is from Win32. Query the overlapped structure to get the winsock error.
|
||||
@@ -235,12 +252,17 @@ func (f *win32File) Read(b []byte) (int, error) {
|
||||
runtime.KeepAlive(b)
|
||||
|
||||
// Handle EOF conditions.
|
||||
if err == nil && n == 0 && len(b) != 0 {
|
||||
return 0, io.EOF
|
||||
} else if err == windows.ERROR_BROKEN_PIPE { //nolint:errorlint // err is Errno
|
||||
switch err {
|
||||
case windows.ERROR_BROKEN_PIPE: //nolint:errorlint // err is Errno
|
||||
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.
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
@@ -442,7 +441,7 @@ func canRedial(err error) bool {
|
||||
|
||||
func (conn *HvsockConn) opErr(op string, err error) error {
|
||||
// translate from "file closed" to "socket closed"
|
||||
if errors.Is(err, ErrFileClosed) {
|
||||
if errors.Is(err, os.ErrClosed) {
|
||||
err = socket.ErrSocketClosed
|
||||
}
|
||||
return &net.OpError{Op: op, Net: "hvsock", Source: &conn.local, Addr: &conn.remote, Err: err}
|
||||
|
||||
+1
-1
@@ -243,7 +243,7 @@ const (
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew
|
||||
func GetFinalPathNameByHandle(h windows.Handle, flags GetFinalPathFlag) (string, error) {
|
||||
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 {
|
||||
n, err := windows.GetFinalPathNameByHandle(h, b.Pointer(), b.Cap(), uint32(flags))
|
||||
if err != nil {
|
||||
|
||||
+2
-6
@@ -15,12 +15,8 @@ var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
errERROR_IO_PENDING error = windows.ERROR_IO_PENDING
|
||||
errERROR_EINVAL error = syscall.EINVAL
|
||||
)
|
||||
|
||||
@@ -30,7 +26,7 @@ func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return errERROR_EINVAL
|
||||
case errnoERROR_IO_PENDING:
|
||||
case windows.ERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
return e
|
||||
|
||||
+2
-6
@@ -15,12 +15,8 @@ var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
errERROR_IO_PENDING error = windows.ERROR_IO_PENDING
|
||||
errERROR_EINVAL error = syscall.EINVAL
|
||||
)
|
||||
|
||||
@@ -30,7 +26,7 @@ func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return errERROR_EINVAL
|
||||
case errnoERROR_IO_PENDING:
|
||||
case windows.ERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
return e
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
// TODO: worth exporting and using in mkwinsyscall?
|
||||
|
||||
// Uint16BufferSize is the buffer size in the pool, chosen somewhat arbitrarily to accommodate
|
||||
// MinWStringCap is the buffer size in the pool, chosen somewhat arbitrarily to accommodate
|
||||
// large path strings:
|
||||
// MAX_PATH (260) + size of volume GUID prefix (49) + null terminator = 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
|
||||
// stores.
|
||||
var pathPool = sync.Pool{ // if go1.18+ adds Pool[T], use that to store []uint16 directly
|
||||
New: func() interface{} {
|
||||
New: func() any {
|
||||
b := make([]uint16, MinWStringCap)
|
||||
return &b
|
||||
},
|
||||
|
||||
+31
-30
@@ -1,5 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
@@ -11,6 +10,7 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
@@ -102,12 +102,10 @@ func (status ntStatus) Err() error {
|
||||
return rtlNtStatusToDosError(status)
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrPipeListenerClosed is returned for pipe operations on listeners that have been closed.
|
||||
ErrPipeListenerClosed = net.ErrClosed
|
||||
|
||||
errPipeWriteClosed = errors.New("pipe has been closed for write")
|
||||
)
|
||||
// ErrPipeListenerClosed is returned for pipe operations on listeners that have been closed.
|
||||
//
|
||||
// Deprecated: use [net.ErrClosed] instead.
|
||||
var ErrPipeListenerClosed = net.ErrClosed
|
||||
|
||||
type win32Pipe struct {
|
||||
*win32File
|
||||
@@ -144,9 +142,10 @@ func (f *win32Pipe) Disconnect() error {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if f.writeClosed {
|
||||
return errPipeWriteClosed
|
||||
return io.ErrClosedPipe
|
||||
}
|
||||
err := f.win32File.Flush()
|
||||
if err != nil {
|
||||
@@ -161,10 +160,10 @@ func (f *win32MessageBytePipe) CloseWrite() error {
|
||||
}
|
||||
|
||||
// Write writes bytes to a message pipe in byte mode. Zero-byte writes are ignored, since
|
||||
// they are used to implement CloseWrite().
|
||||
// they are used to implement CloseWrite. It returns [io.ErrClosedPipe] if the write side is closed.
|
||||
func (f *win32MessageBytePipe) Write(b []byte) (int, error) {
|
||||
if f.writeClosed {
|
||||
return 0, errPipeWriteClosed
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return 0, nil
|
||||
@@ -179,14 +178,15 @@ func (f *win32MessageBytePipe) Read(b []byte) (int, error) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n, err := f.win32File.Read(b)
|
||||
if err == io.EOF { //nolint:errorlint
|
||||
switch err {
|
||||
case io.EOF: //nolint:errorlint // error is not wrapped.
|
||||
// If this was the result of a zero-byte read, then
|
||||
// it is possible that the read was due to a zero-size
|
||||
// message. Since we are simulating CloseWrite with a
|
||||
// zero-byte message, ensure that all future Read() calls
|
||||
// also return EOF.
|
||||
f.readEOF = true
|
||||
} else if err == windows.ERROR_MORE_DATA { //nolint:errorlint // err is Errno
|
||||
case windows.ERROR_MORE_DATA:
|
||||
// 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
|
||||
// this package presents all named pipes as byte streams.
|
||||
@@ -316,8 +316,9 @@ type win32PipeListener struct {
|
||||
path string
|
||||
config PipeConfig
|
||||
acceptCh chan (chan acceptResponse)
|
||||
closeCh chan int
|
||||
doneCh chan int
|
||||
closeOnce sync.Once
|
||||
closeCh chan struct{} // closed (never sent on) to broadcast listener shutdown
|
||||
doneCh chan struct{}
|
||||
}
|
||||
|
||||
func makeServerPipeHandle(path string, sd []byte, c *PipeConfig, first bool) (windows.Handle, error) {
|
||||
@@ -344,7 +345,7 @@ func makeServerPipeHandle(path string, sd []byte, c *PipeConfig, first bool) (wi
|
||||
// The security descriptor is only needed for the first pipe.
|
||||
if first {
|
||||
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))
|
||||
sdb, err := windows.LocalAlloc(0, l)
|
||||
if err != nil {
|
||||
@@ -445,13 +446,14 @@ func (l *win32PipeListener) makeConnectedServerPipe() (*win32File, error) {
|
||||
p = nil
|
||||
}
|
||||
case <-l.closeCh:
|
||||
// Abort the connect request by closing the handle.
|
||||
p.Close()
|
||||
// Abort the connect request by closing the handle. Listener closure is
|
||||
// authoritative: ConnectNamedPipe may race the handle close and report a
|
||||
// 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
|
||||
err = <-ch
|
||||
if err == nil || err == ErrFileClosed { //nolint:errorlint // err is Errno
|
||||
err = ErrPipeListenerClosed
|
||||
}
|
||||
<-ch
|
||||
err = net.ErrClosed
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
@@ -476,7 +478,7 @@ func (l *win32PipeListener) listenerRoutine() {
|
||||
}
|
||||
}
|
||||
responseCh <- acceptResponse{p, err}
|
||||
closed = err == ErrPipeListenerClosed //nolint:errorlint // err is Errno
|
||||
closed = err == net.ErrClosed //nolint:errorlint // err is Errno
|
||||
}
|
||||
}
|
||||
windows.Close(l.firstHandle)
|
||||
@@ -530,8 +532,8 @@ func ListenPipe(path string, c *PipeConfig) (net.Listener, error) {
|
||||
path: path,
|
||||
config: *c,
|
||||
acceptCh: make(chan (chan acceptResponse)),
|
||||
closeCh: make(chan int),
|
||||
doneCh: make(chan int),
|
||||
closeCh: make(chan struct{}),
|
||||
doneCh: make(chan struct{}),
|
||||
}
|
||||
go l.listenerRoutine()
|
||||
return l, nil
|
||||
@@ -568,16 +570,15 @@ func (l *win32PipeListener) Accept() (net.Conn, error) {
|
||||
}
|
||||
return &win32Pipe{win32File: response.f, path: l.path}, nil
|
||||
case <-l.doneCh:
|
||||
return nil, ErrPipeListenerClosed
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
}
|
||||
|
||||
func (l *win32PipeListener) Close() error {
|
||||
select {
|
||||
case l.closeCh <- 1:
|
||||
<-l.doneCh
|
||||
case <-l.doneCh:
|
||||
}
|
||||
l.closeOnce.Do(func() {
|
||||
close(l.closeCh)
|
||||
})
|
||||
<-l.doneCh
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
//go:generate go run golang.org/x/tools/cmd/stringer -type=Variant -trimprefix=Variant -linecomment
|
||||
//go:generate go tool stringer -type=Variant -trimprefix=Variant -linecomment
|
||||
|
||||
// Variant specifies which GUID variant (or "type") of the GUID. It determines
|
||||
// how the entirety of the rest of the GUID is interpreted.
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package guid
|
||||
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package guid
|
||||
|
||||
|
||||
+3
-2
@@ -20,8 +20,9 @@ const _Variant_name = "UnknownNCSRFC 4122MicrosoftFuture"
|
||||
var _Variant_index = [...]uint8{0, 7, 10, 18, 27, 33}
|
||||
|
||||
func (i Variant) String() string {
|
||||
if i >= Variant(len(_Variant_index)-1) {
|
||||
idx := int(i) - 0
|
||||
if i < 0 || idx >= len(_Variant_index)-1 {
|
||||
return "Variant(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _Variant_name[_Variant_index[i]:_Variant_index[i+1]]
|
||||
return _Variant_name[_Variant_index[idx]:_Variant_index[idx+1]]
|
||||
}
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
|
||||
+55
-6
@@ -1,11 +1,11 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
@@ -15,6 +15,9 @@ import (
|
||||
const (
|
||||
reparseTagMountPoint = 0xA0000003
|
||||
reparseTagSymlink = 0xA000000C
|
||||
reparseTagLxSymlink = 0xA000001D // WSL/MSYS2 native symlinks
|
||||
|
||||
lxSymlinkVersion = 2 // LX symlink format version
|
||||
)
|
||||
|
||||
type reparseDataBuffer struct {
|
||||
@@ -31,6 +34,7 @@ type reparseDataBuffer struct {
|
||||
type ReparsePoint struct {
|
||||
Target string
|
||||
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
|
||||
@@ -51,14 +55,19 @@ func DecodeReparsePoint(b []byte) (*ReparsePoint, error) {
|
||||
}
|
||||
|
||||
func DecodeReparsePointData(tag uint32, b []byte) (*ReparsePoint, error) {
|
||||
isMountPoint := false
|
||||
switch tag {
|
||||
case reparseTagMountPoint:
|
||||
isMountPoint = true
|
||||
return decodeWindowsReparsePointData(b, true)
|
||||
case reparseTagSymlink:
|
||||
return decodeWindowsReparsePointData(b, false)
|
||||
case reparseTagLxSymlink:
|
||||
return decodeLxReparsePointData(b)
|
||||
default:
|
||||
return nil, &UnsupportedReparsePointError{tag}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeWindowsReparsePointData(b []byte, isMountPoint bool) (*ReparsePoint, error) {
|
||||
nameOffset := 8 + binary.LittleEndian.Uint16(b[4:6])
|
||||
if !isMountPoint {
|
||||
nameOffset += 4
|
||||
@@ -69,16 +78,56 @@ func DecodeReparsePointData(tag uint32, b []byte) (*ReparsePoint, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ReparsePoint{string(utf16.Decode(name)), isMountPoint}, nil
|
||||
return &ReparsePoint{Target: string(utf16.Decode(name)), IsMountPoint: isMountPoint, IsLxSymlink: false}, 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 {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
||||
}
|
||||
|
||||
// EncodeReparsePoint encodes a Win32 REPARSE_DATA_BUFFER structure describing a symlink or
|
||||
// mount point.
|
||||
// EncodeReparsePoint encodes a Win32 REPARSE_DATA_BUFFER structure describing a symlink,
|
||||
// mount point, or LX symlink.
|
||||
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.
|
||||
var ntTarget string
|
||||
relative := false
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
|
||||
+2
-6
@@ -15,12 +15,8 @@ var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
errERROR_IO_PENDING error = windows.ERROR_IO_PENDING
|
||||
errERROR_EINVAL error = syscall.EINVAL
|
||||
)
|
||||
|
||||
@@ -30,7 +26,7 @@ func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return errERROR_EINVAL
|
||||
case errnoERROR_IO_PENDING:
|
||||
case windows.ERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
return e
|
||||
|
||||
+63
-25
@@ -1,32 +1,34 @@
|
||||
An implementation of grapheme cluster boundaries from [Unicode text segmentation](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries) (UAX 29), for Unicode version 15.0.0.
|
||||
An implementation of grapheme cluster boundaries from [Unicode text segmentation](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries) (UAX 29), for Unicode 17.
|
||||
|
||||
[](https://pkg.go.dev/github.com/clipperhouse/uax29/v2/graphemes)
|
||||

|
||||

|
||||
|
||||
## Quick start
|
||||
|
||||
```
|
||||
go get "github.com/clipperhouse/uax29/v2/graphemes"
|
||||
go get github.com/clipperhouse/uax29/v2/graphemes
|
||||
```
|
||||
|
||||
```go
|
||||
import "github.com/clipperhouse/uax29/v2/graphemes"
|
||||
|
||||
text := "Hello, 世界. Nice dog! 👍🐶"
|
||||
g := graphemes.FromString(text)
|
||||
|
||||
tokens := graphemes.FromString(text)
|
||||
|
||||
for tokens.Next() { // Next() returns true until end of data
|
||||
fmt.Println(tokens.Value()) // Do something with the current grapheme
|
||||
for g.Next() { // Next() returns true until end of data
|
||||
fmt.Println(g.Value()) // Do something with the current grapheme
|
||||
}
|
||||
```
|
||||
|
||||
[](https://pkg.go.dev/github.com/clipperhouse/uax29/v2/graphemes)
|
||||
|
||||
_A grapheme is a “single visible character”, which might be a simple as a single letter, or a complex emoji that consists of several Unicode code points._
|
||||
|
||||
## Conformance
|
||||
|
||||
We use the Unicode [test suite](https://unicode.org/reports/tr41/tr41-26.html#Tests29). Status:
|
||||
We use the Unicode [test suite](https://unicode.org/reports/tr41/tr41-36.html#Tests29).
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## APIs
|
||||
|
||||
@@ -34,11 +36,10 @@ We use the Unicode [test suite](https://unicode.org/reports/tr41/tr41-26.html#Te
|
||||
|
||||
```go
|
||||
text := "Hello, 世界. Nice dog! 👍🐶"
|
||||
g := graphemes.FromString(text)
|
||||
|
||||
tokens := graphemes.FromString(text)
|
||||
|
||||
for tokens.Next() { // Next() returns true until end of data
|
||||
fmt.Println(tokens.Value()) // Do something with the current grapheme
|
||||
for g.Next() { // Next() returns true until end of data
|
||||
fmt.Println(g.Value()) // Do something with the current grapheme
|
||||
}
|
||||
```
|
||||
|
||||
@@ -47,15 +48,15 @@ for tokens.Next() { // Next() returns true until end of data
|
||||
`FromReader` embeds a [`bufio.Scanner`](https://pkg.go.dev/bufio#Scanner), so just use those methods.
|
||||
|
||||
```go
|
||||
r := getYourReader() // from a file or network maybe
|
||||
tokens := graphemes.FromReader(r)
|
||||
r := getYourReader() // from a file or network maybe
|
||||
g := graphemes.FromReader(r)
|
||||
|
||||
for tokens.Scan() { // Scan() returns true until error or EOF
|
||||
fmt.Println(tokens.Text()) // Do something with the current grapheme
|
||||
for g.Scan() { // Scan() returns true until error or EOF
|
||||
fmt.Println(g.Text()) // Do something with the current grapheme
|
||||
}
|
||||
|
||||
if tokens.Err() != nil { // Check the error
|
||||
log.Fatal(tokens.Err())
|
||||
if g.Err() != nil { // Check the error
|
||||
log.Fatal(g.Err())
|
||||
}
|
||||
```
|
||||
|
||||
@@ -64,16 +65,53 @@ if tokens.Err() != nil { // Check the error
|
||||
```go
|
||||
b := []byte("Hello, 世界. Nice dog! 👍🐶")
|
||||
|
||||
tokens := graphemes.FromBytes(b)
|
||||
g := graphemes.FromBytes(b)
|
||||
|
||||
for tokens.Next() { // Next() returns true until end of data
|
||||
fmt.Println(tokens.Value()) // Do something with the current grapheme
|
||||
for g.Next() { // Next() returns true until end of data
|
||||
fmt.Println(g.Value()) // Do something with the current grapheme
|
||||
}
|
||||
```
|
||||
|
||||
### Performance
|
||||
### ANSI escape sequences
|
||||
|
||||
On a Mac M2 laptop, we see around 200MB/s, or around 100 million graphemes per second. You should see ~constant memory, and no allocations.
|
||||
By the UAX 29 specification, ANSI escape sequences are not grapheme clusters. To treat 7-bit ANSI escape sequences as a single cluster, set `AnsiEscapeSequences` to true.
|
||||
|
||||
```go
|
||||
text := "Hello, \x1b[31mworld\x1b[0m!"
|
||||
g := graphemes.FromString(text)
|
||||
g.AnsiEscapeSequences = true
|
||||
|
||||
for g.Next() {
|
||||
fmt.Println(g.Value())
|
||||
}
|
||||
```
|
||||
|
||||
To also parse 8-bit C1 controls (non-UTF-8 bytes), set `AnsiEscapeSequences8Bit` to true.
|
||||
|
||||
```go
|
||||
g.AnsiEscapeSequences = true // 7-bit forms (ESC ...)
|
||||
g.AnsiEscapeSequences8Bit = true // 8-bit C1 forms (0x80-0x9F), not valid UTF-8
|
||||
```
|
||||
|
||||
For ESC-initiated (7-bit) control strings, only 7-bit terminators are recognized.
|
||||
For C1-initiated (8-bit) control strings, only C1 ST (`0x9C`) is recognized as ST.
|
||||
|
||||
We implement [ECMA-48](https://ecma-international.org/publications-and-standards/standards/ecma-48/) control codes in both 7-bit and 8-bit representations. 8-bit control codes are not UTF-8 encoded and are not valid UTF-8, caveat emptor.
|
||||
|
||||
### Benchmarks
|
||||
|
||||
```
|
||||
goos: darwin
|
||||
goarch: arm64
|
||||
pkg: github.com/clipperhouse/uax29/graphemes/comparative
|
||||
cpu: Apple M2
|
||||
|
||||
BenchmarkGraphemesMixed/clipperhouse/uax29-8 142635 ns/op 245.12 MB/s 0 B/op 0 allocs/op
|
||||
BenchmarkGraphemesMixed/rivo/uniseg-8 2018284 ns/op 17.32 MB/s 0 B/op 0 allocs/op
|
||||
|
||||
BenchmarkGraphemesASCII/clipperhouse/uax29-8 8846 ns/op 508.73 MB/s 0 B/op 0 allocs/op
|
||||
BenchmarkGraphemesASCII/rivo/uniseg-8 366760 ns/op 12.27 MB/s 0 B/op 0 allocs/op
|
||||
```
|
||||
|
||||
### Invalid inputs
|
||||
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package graphemes
|
||||
|
||||
// ansiEscapeLength returns the byte length of a valid 7-bit ANSI escape
|
||||
// sequence at the start of data, or 0 if none.
|
||||
//
|
||||
// Recognized forms (ECMA-48 / ISO 6429):
|
||||
// - CSI: ESC [ then parameter bytes (0x30-0x3F), intermediate (0x20-0x2F), final (0x40-0x7E)
|
||||
// - OSC: ESC ] then payload until BEL (0x07), 7-bit ST (ESC \), CAN (0x18), or SUB (0x1A)
|
||||
// - DCS, SOS, PM, APC: ESC P/X/^/_ then payload until 7-bit ST (ESC \), CAN, or SUB
|
||||
// - Two-byte: ESC + Fe/Fs (0x40-0x7E excluding above), or Fp (0x30-0x3F), or nF (0x20-0x2F then final)
|
||||
func ansiEscapeLength[T ~string | ~[]byte](data T) int {
|
||||
n := len(data)
|
||||
if n < 2 || data[0] != esc {
|
||||
return 0
|
||||
}
|
||||
|
||||
b1 := data[1]
|
||||
switch b1 {
|
||||
case '[': // CSI
|
||||
body := csiBodyLength(data[2:])
|
||||
if body == 0 {
|
||||
return 0
|
||||
}
|
||||
return 2 + body
|
||||
case ']': // OSC - allows BEL or 7-bit ST terminator
|
||||
body := oscLength(data[2:])
|
||||
if body < 0 {
|
||||
return 0
|
||||
}
|
||||
return 2 + body
|
||||
case 'P', 'X', '^', '_': // DCS, SOS, PM, APC
|
||||
body := stSequenceLength(data[2:])
|
||||
if body < 0 {
|
||||
return 0
|
||||
}
|
||||
return 2 + body
|
||||
}
|
||||
|
||||
if b1 >= 0x40 && b1 <= 0x7E {
|
||||
// Fe/Fs two-byte; [ ] P X ^ _ handled above
|
||||
return 2
|
||||
}
|
||||
if b1 >= 0x30 && b1 <= 0x3F {
|
||||
// Fp (private) two-byte
|
||||
return 2
|
||||
}
|
||||
if b1 >= 0x20 && b1 <= 0x2F {
|
||||
// nF: intermediates then one final (0x30-0x7E)
|
||||
i := 2
|
||||
for i < n && data[i] >= 0x20 && data[i] <= 0x2F {
|
||||
i++
|
||||
}
|
||||
if i < n && data[i] >= 0x30 && data[i] <= 0x7E {
|
||||
return i + 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// csiBodyLength returns the length of the CSI body (param/intermediate/final bytes).
|
||||
// data is the slice after "ESC [".
|
||||
// Per ECMA-48, the CSI body has the form:
|
||||
//
|
||||
// parameters (0x30–0x3F)*, intermediates (0x20–0x2F)*, final (0x40–0x7E)
|
||||
//
|
||||
// Once an intermediate byte is seen, subsequent parameter bytes are invalid.
|
||||
func csiBodyLength[T ~string | ~[]byte](data T) int {
|
||||
seenIntermediate := false
|
||||
for i := 0; i < len(data); i++ {
|
||||
b := data[i]
|
||||
if b >= 0x30 && b <= 0x3F {
|
||||
if seenIntermediate {
|
||||
return 0
|
||||
}
|
||||
continue
|
||||
}
|
||||
if b >= 0x20 && b <= 0x2F {
|
||||
seenIntermediate = true
|
||||
continue
|
||||
}
|
||||
if b >= 0x40 && b <= 0x7E {
|
||||
return i + 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// oscLength returns the length of the OSC body.
|
||||
// data is the slice after "ESC ]".
|
||||
//
|
||||
// Returns:
|
||||
// - n >= 0: consumed body length (includes BEL/ST terminator when present)
|
||||
// - -1: not terminated in the provided data
|
||||
//
|
||||
// OSC accepts BEL (0x07) or 7-bit ST (ESC \) as terminators by widespread convention.
|
||||
// Per ECMA-48, CAN (0x18) and SUB (0x1A) cancel the control string; in that
|
||||
// case they are not part of the OSC sequence length.
|
||||
func oscLength[T ~string | ~[]byte](data T) int {
|
||||
for i := 0; i < len(data); i++ {
|
||||
b := data[i]
|
||||
if b == bel {
|
||||
return i + 1
|
||||
}
|
||||
if b == can || b == sub {
|
||||
return i
|
||||
}
|
||||
if b == esc && i+1 < len(data) && data[i+1] == '\\' {
|
||||
return i + 2
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// stSequenceLength returns the length of a control-string body.
|
||||
// data is the slice after "ESC x".
|
||||
//
|
||||
// Returns:
|
||||
// - n >= 0: consumed body length (includes ST terminator when present)
|
||||
// - -1: not terminated in the provided data
|
||||
//
|
||||
// Used for DCS, SOS, PM, and APC, which per ECMA-48 terminate with ST.
|
||||
// ST here is the 7-bit form (ESC \).
|
||||
// CAN (0x18) and SUB (0x1A) cancel the control string; in that case they are
|
||||
// not part of the sequence length.
|
||||
func stSequenceLength[T ~string | ~[]byte](data T) int {
|
||||
for i := 0; i < len(data); i++ {
|
||||
if data[i] == can || data[i] == sub {
|
||||
return i
|
||||
}
|
||||
if data[i] == esc && i+1 < len(data) && data[i+1] == '\\' {
|
||||
return i + 2
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package graphemes
|
||||
|
||||
// ansiEscapeLength8Bit returns the byte length of a valid 8-bit C1 ANSI
|
||||
// sequence at the start of data, or 0 if none.
|
||||
//
|
||||
// Recognized forms (ECMA-48 / ISO 6429):
|
||||
// - C1 CSI (0x9B) body as parameter/intermediate/final bytes
|
||||
// - C1 OSC (0x9D) body terminated by BEL, C1 ST, CAN, or SUB
|
||||
// - C1 DCS/SOS/PM/APC (0x90/0x98/0x9E/0x9F) body terminated by C1 ST, CAN, or SUB
|
||||
// - Standalone C1 controls (0x80..0x9F not listed above): single byte
|
||||
func ansiEscapeLength8Bit[T ~string | ~[]byte](data T) int {
|
||||
if len(data) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
switch data[0] {
|
||||
case 0x9B: // C1 CSI
|
||||
body := csiBodyLength(data[1:])
|
||||
if body == 0 {
|
||||
return 0
|
||||
}
|
||||
return 1 + body
|
||||
case 0x9D: // C1 OSC
|
||||
body := oscLengthC1(data[1:])
|
||||
if body < 0 {
|
||||
return 0
|
||||
}
|
||||
return 1 + body
|
||||
case 0x90, 0x98, 0x9E, 0x9F: // C1 DCS, SOS, PM, APC
|
||||
body := stSequenceLengthC1(data[1:])
|
||||
if body < 0 {
|
||||
return 0
|
||||
}
|
||||
return 1 + body
|
||||
default:
|
||||
if data[0] >= 0x80 && data[0] <= 0x9F {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// oscLengthC1 returns the length of a C1 OSC body.
|
||||
// data is the slice after the C1 OSC initiator (0x9D).
|
||||
//
|
||||
// Returns:
|
||||
// - n >= 0: consumed body length (includes BEL/ST terminator when present)
|
||||
// - -1: not terminated in the provided data
|
||||
//
|
||||
// Terminators: BEL (0x07) or C1 ST (0x9C).
|
||||
// CAN (0x18) and SUB (0x1A) cancel the control string.
|
||||
func oscLengthC1[T ~string | ~[]byte](data T) int {
|
||||
for i := 0; i < len(data); i++ {
|
||||
b := data[i]
|
||||
if b == bel || b == st {
|
||||
return i + 1
|
||||
}
|
||||
if b == can || b == sub {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// stSequenceLengthC1 parses DCS/SOS/PM/APC bodies that terminate with C1 ST
|
||||
// (0x9C), or are canceled by CAN/SUB.
|
||||
func stSequenceLengthC1[T ~string | ~[]byte](data T) int {
|
||||
for i := 0; i < len(data); i++ {
|
||||
b := data[i]
|
||||
if b == can || b == sub {
|
||||
return i
|
||||
}
|
||||
if b == st {
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
+129
-13
@@ -1,9 +1,44 @@
|
||||
package graphemes
|
||||
|
||||
import "github.com/clipperhouse/uax29/v2/internal/iterators"
|
||||
import "unicode/utf8"
|
||||
|
||||
type Iterator[T iterators.Stringish] struct {
|
||||
*iterators.Iterator[T]
|
||||
// FromString returns an iterator for the grapheme clusters in the input string.
|
||||
// Iterate while Next() is true, and access the grapheme via Value().
|
||||
func FromString(s string) *Iterator[string] {
|
||||
return &Iterator[string]{
|
||||
split: splitFuncString,
|
||||
data: s,
|
||||
}
|
||||
}
|
||||
|
||||
// FromBytes returns an iterator for the grapheme clusters in the input bytes.
|
||||
// Iterate while Next() is true, and access the grapheme via Value().
|
||||
func FromBytes(b []byte) *Iterator[[]byte] {
|
||||
return &Iterator[[]byte]{
|
||||
split: splitFuncBytes,
|
||||
data: b,
|
||||
}
|
||||
}
|
||||
|
||||
// Iterator is a generic iterator for grapheme clusters in strings or byte slices,
|
||||
// with an ASCII hot path optimization.
|
||||
type Iterator[T ~string | ~[]byte] struct {
|
||||
split func(T, bool) (int, T, error)
|
||||
data T
|
||||
pos int
|
||||
start int
|
||||
// AnsiEscapeSequences treats 7-bit ANSI escape sequences (ECMA-48) as
|
||||
// single grapheme clusters when true. The default is false.
|
||||
//
|
||||
// 8-bit controls are not enabled by this option. See [AnsiEscapeSequences8Bit].
|
||||
AnsiEscapeSequences bool
|
||||
// AnsiEscapeSequences8Bit treats 8-bit C1 ANSI escape sequences (ECMA-48) as single
|
||||
// grapheme clusters when true. The default is false.
|
||||
//
|
||||
// 8-bit control bytes are not UTF-8 encoded, i.e. not valid UTF-8. If you
|
||||
// choose this option, you are choosing to interpret non-UTF-8 data, caveat
|
||||
// emptor.
|
||||
AnsiEscapeSequences8Bit bool
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -11,18 +46,99 @@ var (
|
||||
splitFuncBytes = splitFunc[[]byte]
|
||||
)
|
||||
|
||||
// FromString returns an iterator for the grapheme clusters in the input string.
|
||||
// Iterate while Next() is true, and access the grapheme via Value().
|
||||
func FromString(s string) Iterator[string] {
|
||||
return Iterator[string]{
|
||||
iterators.New(splitFuncString, s),
|
||||
const (
|
||||
esc = 0x1B
|
||||
cr = 0x0D
|
||||
bel = 0x07
|
||||
can = 0x18
|
||||
sub = 0x1A
|
||||
st = 0x9C
|
||||
)
|
||||
|
||||
// Next advances the iterator to the next grapheme cluster.
|
||||
// Returns false when there are no more grapheme clusters.
|
||||
func (iter *Iterator[T]) Next() bool {
|
||||
if iter.pos >= len(iter.data) {
|
||||
return false
|
||||
}
|
||||
iter.start = iter.pos
|
||||
|
||||
b := iter.data[iter.pos]
|
||||
if iter.AnsiEscapeSequences && b == esc {
|
||||
if a := ansiEscapeLength(iter.data[iter.pos:]); a > 0 {
|
||||
iter.pos += a
|
||||
return true
|
||||
}
|
||||
}
|
||||
if iter.AnsiEscapeSequences8Bit && b >= 0x80 && b <= 0x9F {
|
||||
if a := ansiEscapeLength8Bit(iter.data[iter.pos:]); a > 0 {
|
||||
iter.pos += a
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// ASCII hot path: any ASCII is one grapheme when next byte is ASCII or end.
|
||||
if b < utf8.RuneSelf && b != cr {
|
||||
if iter.pos+1 >= len(iter.data) || iter.data[iter.pos+1] < utf8.RuneSelf {
|
||||
iter.pos++
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to UAX29 grapheme parsing
|
||||
remaining := iter.data[iter.pos:]
|
||||
advance, _, err := iter.split(remaining, true)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if advance <= 0 {
|
||||
panic("splitFunc returned a zero or negative advance")
|
||||
}
|
||||
iter.pos += advance
|
||||
if iter.pos > len(iter.data) {
|
||||
panic("splitFunc advanced beyond end of data")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// FromBytes returns an iterator for the grapheme clusters in the input bytes.
|
||||
// Iterate while Next() is true, and access the grapheme via Value().
|
||||
func FromBytes(b []byte) Iterator[[]byte] {
|
||||
return Iterator[[]byte]{
|
||||
iterators.New(splitFuncBytes, b),
|
||||
// Value returns the current grapheme cluster.
|
||||
func (iter *Iterator[T]) Value() T {
|
||||
return iter.data[iter.start:iter.pos]
|
||||
}
|
||||
|
||||
// Start returns the byte position of the current grapheme in the original data.
|
||||
func (iter *Iterator[T]) Start() int {
|
||||
return iter.start
|
||||
}
|
||||
|
||||
// End returns the byte position after the current grapheme in the original data.
|
||||
func (iter *Iterator[T]) End() int {
|
||||
return iter.pos
|
||||
}
|
||||
|
||||
// Reset resets the iterator to the beginning of the data.
|
||||
func (iter *Iterator[T]) Reset() {
|
||||
iter.start = 0
|
||||
iter.pos = 0
|
||||
}
|
||||
|
||||
// SetText sets the data for the iterator to operate on, and resets all state.
|
||||
func (iter *Iterator[T]) SetText(data T) {
|
||||
iter.data = data
|
||||
iter.start = 0
|
||||
iter.pos = 0
|
||||
}
|
||||
|
||||
// First returns the first grapheme cluster without advancing the iterator.
|
||||
func (iter *Iterator[T]) First() T {
|
||||
if len(iter.data) == 0 {
|
||||
return iter.data
|
||||
}
|
||||
|
||||
// Use a copy to leverage Next()'s ASCII optimization
|
||||
cp := *iter
|
||||
cp.pos = 0
|
||||
cp.start = 0
|
||||
cp.Next()
|
||||
return cp.Value()
|
||||
}
|
||||
|
||||
+39
-8
@@ -2,8 +2,6 @@ package graphemes
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
|
||||
"github.com/clipperhouse/uax29/v2/internal/iterators"
|
||||
)
|
||||
|
||||
// is determines if lookup intersects propert(ies)
|
||||
@@ -13,12 +11,22 @@ func (lookup property) is(properties property) bool {
|
||||
|
||||
const _Ignore = _Extend
|
||||
|
||||
// incbState tracks state for GB9c rule (Indic conjunct clusters)
|
||||
// Pattern: Consonant (Extend|Linker)* Linker (Extend|Linker)* × Consonant
|
||||
type incbState int
|
||||
|
||||
const (
|
||||
incbNone incbState = iota // initial/reset
|
||||
incbConsonant // seen Consonant, awaiting Linker
|
||||
incbLinker // seen Consonant and Linker (conjunct ready)
|
||||
)
|
||||
|
||||
// SplitFunc is a bufio.SplitFunc implementation of Unicode grapheme cluster segmentation, for use with bufio.Scanner.
|
||||
//
|
||||
// See https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries.
|
||||
var SplitFunc bufio.SplitFunc = splitFunc[[]byte]
|
||||
|
||||
func splitFunc[T iterators.Stringish](data T, atEOF bool) (advance int, token T, err error) {
|
||||
func splitFunc[T ~string | ~[]byte](data T, atEOF bool) (advance int, token T, err error) {
|
||||
var empty T
|
||||
if len(data) == 0 {
|
||||
return 0, empty, nil
|
||||
@@ -30,6 +38,9 @@ func splitFunc[T iterators.Stringish](data T, atEOF bool) (advance int, token T,
|
||||
var lastLastExIgnore property = 0 // "last one before that"
|
||||
var regionalIndicatorCount int
|
||||
|
||||
// GB9c state: tracking Indic conjunct clusters
|
||||
var incb incbState
|
||||
|
||||
// Rules are usually of the form Cat1 × Cat2; "current" refers to the first property
|
||||
// to the right of the ×, from which we look back or forward
|
||||
|
||||
@@ -76,6 +87,23 @@ func splitFunc[T iterators.Stringish](data T, atEOF bool) (advance int, token T,
|
||||
lastExIgnore = last
|
||||
}
|
||||
|
||||
// Update GB9c state based on what we just advanced past
|
||||
if last.is(_InCBConsonant | _InCBLinker | _InCBExtend) {
|
||||
switch {
|
||||
case last.is(_InCBConsonant):
|
||||
if incb != incbLinker {
|
||||
incb = incbConsonant
|
||||
}
|
||||
case last.is(_InCBLinker):
|
||||
if incb >= incbConsonant {
|
||||
incb = incbLinker
|
||||
}
|
||||
// case last.is(_InCBExtend): stay in current state
|
||||
}
|
||||
} else {
|
||||
incb = incbNone
|
||||
}
|
||||
|
||||
current, w = lookup(data[pos:])
|
||||
if w == 0 {
|
||||
if atEOF {
|
||||
@@ -141,11 +169,14 @@ func splitFunc[T iterators.Stringish](data T, atEOF bool) (advance int, token T,
|
||||
}
|
||||
|
||||
// https://unicode.org/reports/tr29/#GB9c
|
||||
// TODO(clipperhouse):
|
||||
// It appears to be added in Unicode 15.1.0:
|
||||
// https://unicode.org/versions/Unicode15.1.0/#Migration
|
||||
// This package currently supports Unicode 15.0.0, so
|
||||
// out of scope for now
|
||||
// Do not break within certain combinations with Indic_Conjunct_Break (InCB)=Linker.
|
||||
if incb == incbLinker && current.is(_InCBConsonant) {
|
||||
// After matching the pattern, reset state to start tracking a new pattern
|
||||
// The current Consonant becomes the start of the new pattern
|
||||
incb = incbConsonant
|
||||
pos += w
|
||||
continue
|
||||
}
|
||||
|
||||
// https://unicode.org/reports/tr29/#GB11
|
||||
if current.is(_ExtendedPictographic) && last.is(_ZWJ) && lastLastExIgnore.is(_ExtendedPictographic) {
|
||||
|
||||
+1323
-1015
File diff suppressed because it is too large
Load Diff
-85
@@ -1,85 +0,0 @@
|
||||
package iterators
|
||||
|
||||
type Stringish interface {
|
||||
[]byte | string
|
||||
}
|
||||
|
||||
type SplitFunc[T Stringish] func(T, bool) (int, T, error)
|
||||
|
||||
// Iterator is a generic iterator for words that are either []byte or string.
|
||||
// Iterate while Next() is true, and access the word via Value().
|
||||
type Iterator[T Stringish] struct {
|
||||
split SplitFunc[T]
|
||||
data T
|
||||
start int
|
||||
pos int
|
||||
}
|
||||
|
||||
// New creates a new Iterator for the given data and SplitFunc.
|
||||
func New[T Stringish](split SplitFunc[T], data T) *Iterator[T] {
|
||||
return &Iterator[T]{
|
||||
split: split,
|
||||
data: data,
|
||||
}
|
||||
}
|
||||
|
||||
// SetText sets the text for the iterator to operate on, and resets all state.
|
||||
func (iter *Iterator[T]) SetText(data T) {
|
||||
iter.data = data
|
||||
iter.start = 0
|
||||
iter.pos = 0
|
||||
}
|
||||
|
||||
// Split sets the SplitFunc for the Iterator.
|
||||
func (iter *Iterator[T]) Split(split SplitFunc[T]) {
|
||||
iter.split = split
|
||||
}
|
||||
|
||||
// Next advances the iterator to the next token. It returns false when there
|
||||
// are no remaining tokens or an error occurred.
|
||||
func (iter *Iterator[T]) Next() bool {
|
||||
if iter.pos == len(iter.data) {
|
||||
return false
|
||||
}
|
||||
if iter.pos > len(iter.data) {
|
||||
panic("SplitFunc advanced beyond the end of the data")
|
||||
}
|
||||
|
||||
iter.start = iter.pos
|
||||
|
||||
advance, _, err := iter.split(iter.data[iter.pos:], true)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if advance <= 0 {
|
||||
panic("SplitFunc returned a zero or negative advance")
|
||||
}
|
||||
|
||||
iter.pos += advance
|
||||
if iter.pos > len(iter.data) {
|
||||
panic("SplitFunc advanced beyond the end of the data")
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Value returns the current token.
|
||||
func (iter *Iterator[T]) Value() T {
|
||||
return iter.data[iter.start:iter.pos]
|
||||
}
|
||||
|
||||
// Start returns the byte position of the current token in the original data.
|
||||
func (iter *Iterator[T]) Start() int {
|
||||
return iter.start
|
||||
}
|
||||
|
||||
// End returns the byte position after the current token in the original data.
|
||||
func (iter *Iterator[T]) End() int {
|
||||
return iter.pos
|
||||
}
|
||||
|
||||
// Reset resets the iterator to the beginning of the data.
|
||||
func (iter *Iterator[T]) Reset() {
|
||||
iter.start = 0
|
||||
iter.pos = 0
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
+35
-15
@@ -1,30 +1,50 @@
|
||||
version: "2"
|
||||
linters:
|
||||
enable:
|
||||
- exportloopref # Checks for pointers to enclosing loop variables
|
||||
- gofmt
|
||||
- goimports
|
||||
- copyloopvar
|
||||
- dupword
|
||||
- gosec
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- nolintlint
|
||||
- revive
|
||||
- staticcheck
|
||||
- tenv # Detects using os.Setenv instead of t.Setenv since Go 1.17
|
||||
- unconvert
|
||||
- unused
|
||||
- vet
|
||||
- dupword # Checks for duplicate words in the source code
|
||||
disable:
|
||||
- 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:
|
||||
timeout: 5m
|
||||
skip-dirs:
|
||||
- api
|
||||
- cluster
|
||||
- design
|
||||
- docs
|
||||
- docs/man
|
||||
- releases
|
||||
- reports
|
||||
- test # e2e scripts
|
||||
|
||||
+61
-9
@@ -40,8 +40,11 @@ package log
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
lslog "github.com/sirupsen/logrus/hooks/slog"
|
||||
)
|
||||
|
||||
// G is a shorthand for [GetLogger].
|
||||
@@ -108,10 +111,17 @@ const (
|
||||
PanicLevel Level = logrus.PanicLevel
|
||||
)
|
||||
|
||||
// SetLevel sets log level globally. It returns an error if the given
|
||||
// level is not supported.
|
||||
// levelValue is a log level accepted by [SetLevel].
|
||||
type levelValue interface {
|
||||
string | Level | slog.Level | lslog.Level | lslog.SlogLevel
|
||||
}
|
||||
|
||||
// SetLevel sets the global log level.
|
||||
//
|
||||
// level can be one of:
|
||||
// The level may be specified as a string, a [Level], a [slog.Level], an
|
||||
// [lslog.Level], or an [lslog.SlogLevel].
|
||||
//
|
||||
// String levels are parsed using [logrus.ParseLevel] and may be one of:
|
||||
//
|
||||
// - "trace" ([TraceLevel])
|
||||
// - "debug" ([DebugLevel])
|
||||
@@ -120,10 +130,34 @@ const (
|
||||
// - "error" ([ErrorLevel])
|
||||
// - "fatal" ([FatalLevel])
|
||||
// - "panic" ([PanicLevel])
|
||||
func SetLevel(level string) error {
|
||||
lvl, err := logrus.ParseLevel(level)
|
||||
if err != nil {
|
||||
return err
|
||||
//
|
||||
// slog levels are mapped to Logrus levels using the mapping defined by
|
||||
// [lslog.SlogLevel]. [lslog.Level] values are accepted directly as their
|
||||
// underlying Logrus level.
|
||||
//
|
||||
// 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)
|
||||
@@ -149,21 +183,39 @@ const (
|
||||
|
||||
// SetFormat sets the log output format ([TextFormat] or [JSONFormat]).
|
||||
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 {
|
||||
case TextFormat:
|
||||
L.Logger.SetFormatter(&logrus.TextFormatter{
|
||||
TimestampFormat: RFC3339NanoFixed,
|
||||
FullTimestamp: true,
|
||||
})
|
||||
return nil
|
||||
case JSONFormat:
|
||||
L.Logger.SetFormatter(&logrus.JSONFormatter{
|
||||
TimestampFormat: RFC3339NanoFixed,
|
||||
})
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unknown log format: %s", format)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithLogger returns a new context with the provided logger. Use in
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
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 }
|
||||
+5
-1
@@ -9,13 +9,17 @@ import (
|
||||
// NewTCPSocket creates a TCP socket listener with the specified address and
|
||||
// the specified tls configuration. If TLSConfig is set, will encapsulate the
|
||||
// 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) {
|
||||
l, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tlsConfig != nil {
|
||||
tlsConfig.NextProtos = []string{"http/1.1"}
|
||||
if len(tlsConfig.NextProtos) == 0 {
|
||||
tlsConfig = tlsConfig.Clone()
|
||||
tlsConfig.NextProtos = []string{"http/1.1"}
|
||||
}
|
||||
l = tls.NewListener(l, tlsConfig)
|
||||
}
|
||||
return l, nil
|
||||
|
||||
+2
@@ -6,7 +6,9 @@ This package provides various compression algorithms.
|
||||
* [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).
|
||||
* [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.
|
||||
* [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.
|
||||
* [pgzip](https://github.com/klauspost/pgzip) is a separate package that provides a very fast parallel gzip implementation.
|
||||
|
||||
|
||||
+63
@@ -9,6 +9,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/bits"
|
||||
|
||||
"github.com/klauspost/compress/internal/le"
|
||||
)
|
||||
@@ -210,6 +211,68 @@ func (b *bitReaderShifted) remaining() uint {
|
||||
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.
|
||||
func (b *bitReaderShifted) close() error {
|
||||
// Release reference.
|
||||
|
||||
+87
-8
@@ -8,36 +8,99 @@ import (
|
||||
)
|
||||
|
||||
// decompress4x_main_loop_amd64 is an x86 assembler implementation
|
||||
// of Decompress4X when tablelog > 8.
|
||||
// of Decompress4X when tablelog > 8, decoding fastSymbols symbols per
|
||||
// stream between bit container reloads.
|
||||
//
|
||||
//go:noescape
|
||||
func decompress4x_main_loop_amd64(ctx *decompress4xContext)
|
||||
|
||||
// decompress4x_8b_main_loop_amd64 is an x86 assembler implementation
|
||||
// of Decompress4X when tablelog <= 8 which decodes 4 entries
|
||||
// per loop.
|
||||
// of Decompress4X when tablelog <= 8, decoding fast8bSymbols symbols
|
||||
// per stream between bit container reloads.
|
||||
//
|
||||
//go:noescape
|
||||
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
|
||||
// of Decompress1X when tablelog > 8.
|
||||
// of Decompress1X when tablelog > 8, decoding fastSymbols symbols
|
||||
// between bit container reloads.
|
||||
//
|
||||
//go:noescape
|
||||
func decompress1x_main_loop_amd64(ctx *decompress1xContext)
|
||||
|
||||
// decompress1x_main_loop_bmi2 is an x86 with BMI2 assembler implementation
|
||||
// of Decompress1X when tablelog > 8.
|
||||
// decompress1x_8b_main_loop_amd64 is an x86 assembler implementation
|
||||
// of Decompress1X when tablelog <= 8, decoding fast8bSymbols symbols
|
||||
// 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
|
||||
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) {
|
||||
decompress4x_main_loop_amd64(ctx)
|
||||
if cpuinfo.HasBMI2() {
|
||||
decompress4x_main_loop_bmi2(ctx)
|
||||
} else {
|
||||
decompress4x_main_loop_amd64(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func decompress4x_8b_main_loop_asm(ctx *decompress4xContext) {
|
||||
decompress4x_8b_main_loop_amd64(ctx)
|
||||
if cpuinfo.HasBMI2() {
|
||||
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) {
|
||||
@@ -47,3 +110,19 @@ func decompress1x_main_loop_asm(ctx *decompress1xContext) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
+3261
-705
File diff suppressed because it is too large
Load Diff
+39
-4
@@ -6,24 +6,47 @@
|
||||
package huff0
|
||||
|
||||
// decompress4x_main_loop_arm64 is an arm64 assembler implementation
|
||||
// of Decompress4X when tablelog > 8.
|
||||
// of Decompress4X when tablelog > 8, decoding fastSymbols symbols per
|
||||
// stream between bit container reloads.
|
||||
//
|
||||
//go:noescape
|
||||
func decompress4x_main_loop_arm64(ctx *decompress4xContext)
|
||||
|
||||
// decompress4x_8b_main_loop_arm64 is an arm64 assembler implementation
|
||||
// of Decompress4X when tablelog <= 8 which decodes 4 entries
|
||||
// per loop.
|
||||
// of Decompress4X when tablelog <= 8, decoding fast8bSymbols symbols
|
||||
// per stream between bit container reloads.
|
||||
//
|
||||
//go:noescape
|
||||
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
|
||||
// of Decompress1X when tablelog > 8.
|
||||
// of Decompress1X when tablelog > 8, decoding fastSymbols symbols
|
||||
// between bit container reloads.
|
||||
//
|
||||
//go:noescape
|
||||
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) {
|
||||
decompress4x_main_loop_arm64(ctx)
|
||||
}
|
||||
@@ -32,6 +55,18 @@ func decompress4x_8b_main_loop_asm(ctx *decompress4xContext) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
+1631
-825
File diff suppressed because it is too large
Load Diff
+79
-19
@@ -14,6 +14,10 @@ import (
|
||||
// fallback8BitSize is the size where using Go version is faster.
|
||||
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 {
|
||||
pbr *[4]bitReaderShifted
|
||||
peekBits uint8
|
||||
@@ -21,9 +25,20 @@ type decompress4xContext struct {
|
||||
dstEvery int
|
||||
tbl *dEntrySingle
|
||||
decoded int
|
||||
limit *byte
|
||||
limit *byte // stream 0's output pointer must stay below this
|
||||
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.
|
||||
// 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
|
||||
@@ -72,23 +87,55 @@ func (d *Decoder) Decompress4X(dst, src []byte) ([]byte, error) {
|
||||
|
||||
var decoded int
|
||||
|
||||
if len(out) > 4*4 && !(br[0].off < 4 || br[1].off < 4 || br[2].off < 4 || br[3].off < 4) {
|
||||
nSyms := fastSymbols
|
||||
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{
|
||||
pbr: &br,
|
||||
peekBits: uint8((64 - d.actualTableLog) & 63), // see: bitReaderShifted.peekBitsFast()
|
||||
out: &out[0],
|
||||
dstEvery: dstEvery,
|
||||
tbl: &single[0],
|
||||
limit: &out[dstEvery-4], // Always stop decoding when first buffer gets here to avoid writing OOB on last.
|
||||
limit: &out[limit],
|
||||
// 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],
|
||||
}
|
||||
if use8BitTables {
|
||||
for i := range br {
|
||||
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)
|
||||
} else {
|
||||
default:
|
||||
decompress4x_main_loop_asm(&ctx)
|
||||
}
|
||||
|
||||
decoded = ctx.decoded
|
||||
out = out[decoded/4:]
|
||||
for i := range br {
|
||||
if err := br[i].restoreFromAsm(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Decode remaining.
|
||||
@@ -128,18 +175,20 @@ func (d *Decoder) Decompress4X(dst, src []byte) ([]byte, error) {
|
||||
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 {
|
||||
pbr *bitReaderShifted
|
||||
peekBits uint8
|
||||
out *byte
|
||||
outCap int
|
||||
outCap int // no write reaches out[outCap]
|
||||
tbl *dEntrySingle
|
||||
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.
|
||||
// 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.
|
||||
@@ -158,25 +207,36 @@ func (d *Decoder) Decompress1X(dst, src []byte) ([]byte, error) {
|
||||
const tlSize = 1 << tableLogMax
|
||||
const tlMask = tlSize - 1
|
||||
|
||||
if maxDecodedSize >= 4 {
|
||||
// The asm decodes whole batches of symbols and only re-checks its
|
||||
// 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{
|
||||
pbr: &br,
|
||||
out: &dst[0],
|
||||
outCap: maxDecodedSize,
|
||||
peekBits: uint8((64 - d.actualTableLog) & 63), // see: bitReaderShifted.peekBitsFast()
|
||||
tbl: &d.dt.single[0],
|
||||
ilowest: &src[0],
|
||||
ip: &br.in[br.off],
|
||||
}
|
||||
|
||||
decompress1x_main_loop_asm(&ctx)
|
||||
if ctx.decoded == error_max_decoded_size_exeeded {
|
||||
return nil, ErrMaxDecodedSizeExceeded
|
||||
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)
|
||||
}
|
||||
|
||||
dst = dst[:ctx.decoded]
|
||||
decoded = ctx.decoded
|
||||
}
|
||||
dst = dst[:decoded]
|
||||
|
||||
// br < 8, so uint8 is fine
|
||||
bitsLeft := uint8(br.off)*8 + 64 - br.bitsRead
|
||||
bitsLeft := br.remaining()
|
||||
for bitsLeft > 0 {
|
||||
br.fill()
|
||||
if len(dst) >= maxDecodedSize {
|
||||
@@ -186,7 +246,7 @@ func (d *Decoder) Decompress1X(dst, src []byte) ([]byte, error) {
|
||||
v := d.dt.single[br.peekBitsFast(d.actualTableLog)&tlMask]
|
||||
nBits := uint8(v.entry)
|
||||
br.advance(nBits)
|
||||
bitsLeft -= nBits
|
||||
bitsLeft -= uint(nBits)
|
||||
dst = append(dst, uint8(v.entry>>8))
|
||||
}
|
||||
return dst, br.close()
|
||||
|
||||
+3
@@ -240,9 +240,11 @@ func (b *blockDec) decodeBuf(hist *history) error {
|
||||
b.dst[i] = v
|
||||
}
|
||||
hist.appendKeep(b.dst)
|
||||
hist.decoders.consumeSyncLen(len(b.dst))
|
||||
return nil
|
||||
case blockTypeRaw:
|
||||
hist.appendKeep(b.data)
|
||||
hist.decoders.consumeSyncLen(len(b.data))
|
||||
return nil
|
||||
case blockTypeCompressed:
|
||||
saved := b.dst
|
||||
@@ -487,6 +489,7 @@ func (b *blockDec) decodeCompressed(hist *history) error {
|
||||
}
|
||||
if hist.decoders.nSeqs == 0 {
|
||||
b.dst = append(b.dst, hist.decoders.literals...)
|
||||
hist.decoders.consumeSyncLen(len(hist.decoders.literals))
|
||||
return nil
|
||||
}
|
||||
before := len(hist.decoders.out)
|
||||
|
||||
+81
-44
@@ -7,8 +7,11 @@ package zstd
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"maps"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/klauspost/compress/zstd/internal/xxhash"
|
||||
)
|
||||
@@ -20,7 +23,8 @@ import (
|
||||
// A decoder can safely be re-used even if the previous stream failed.
|
||||
// To release the resources, you must call the Close() function on a decoder.
|
||||
type Decoder struct {
|
||||
o decoderOptions
|
||||
// publishedOptions contains the current immutable decoder options.
|
||||
publishedOptions atomic.Pointer[decoderOptions]
|
||||
|
||||
// Unreferenced decoders, ready for use.
|
||||
decoders chan *blockDec
|
||||
@@ -84,9 +88,10 @@ var (
|
||||
func NewReader(r io.Reader, opts ...DOption) (*Decoder, error) {
|
||||
initPredefined()
|
||||
var d Decoder
|
||||
d.o.setDefault()
|
||||
for _, o := range opts {
|
||||
err := o(&d.o)
|
||||
var o decoderOptions
|
||||
o.setDefault()
|
||||
for _, opt := range opts {
|
||||
err := opt(&o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -99,17 +104,19 @@ func NewReader(r io.Reader, opts ...DOption) (*Decoder, error) {
|
||||
}
|
||||
|
||||
// Initialize dict map if needed.
|
||||
if d.o.dicts == nil {
|
||||
d.o.dicts = make(map[uint32]*dict)
|
||||
if o.dicts == nil {
|
||||
o.dicts = make(map[uint32]*dict)
|
||||
}
|
||||
|
||||
// Create decoders
|
||||
d.decoders = make(chan *blockDec, d.o.concurrent)
|
||||
for i := 0; i < d.o.concurrent; i++ {
|
||||
dec := newBlockDec(d.o.lowMem)
|
||||
dec.localFrame = newFrameDec(d.o)
|
||||
d.decoders = make(chan *blockDec, o.concurrent)
|
||||
for i := 0; i < o.concurrent; i++ {
|
||||
dec := newBlockDec(o.lowMem)
|
||||
dec.localFrame = newFrameDec(o)
|
||||
dec.localFrame.o.dicts = nil
|
||||
d.decoders <- dec
|
||||
}
|
||||
d.publishedOptions.Store(&o)
|
||||
|
||||
if r == nil {
|
||||
return &d, nil
|
||||
@@ -171,6 +178,9 @@ func (d *Decoder) Reset(r io.Reader) error {
|
||||
d.drainOutput()
|
||||
|
||||
d.syncStream.br.r = nil
|
||||
if d.frame != nil {
|
||||
d.frame.o.dicts = nil
|
||||
}
|
||||
if r == nil {
|
||||
d.current.err = ErrDecoderNilInput
|
||||
if len(d.current.b) > 0 {
|
||||
@@ -180,8 +190,10 @@ func (d *Decoder) Reset(r io.Reader) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
o := *d.publishedOptions.Load()
|
||||
|
||||
// If bytes buffer and < 5MB, do sync decoding anyway.
|
||||
if bb, ok := r.(byter); ok && bb.Len() < d.o.decodeBufsBelow && !d.o.limitToCap {
|
||||
if bb, ok := r.(byter); ok && bb.Len() < o.decodeBufsBelow && !o.limitToCap {
|
||||
bb2 := bb
|
||||
if debugDecoder {
|
||||
println("*bytes.Buffer detected, doing sync decode, len:", bb.Len())
|
||||
@@ -193,18 +205,30 @@ func (d *Decoder) Reset(r io.Reader) error {
|
||||
}
|
||||
|
||||
dst, err := d.DecodeAll(b, dst)
|
||||
if err == nil {
|
||||
err = io.EOF
|
||||
if !errors.Is(err, ErrDecoderSizeExceeded) {
|
||||
if err == nil {
|
||||
err = io.EOF
|
||||
}
|
||||
// Save output buffer
|
||||
d.syncStream.dstBuf = dst
|
||||
d.current.b = dst
|
||||
d.current.err = err
|
||||
d.current.flushed = true
|
||||
if debugDecoder {
|
||||
println("sync decode to", len(dst), "bytes, err:", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Save output buffer
|
||||
d.syncStream.dstBuf = dst
|
||||
d.current.b = dst
|
||||
d.current.err = err
|
||||
d.current.flushed = true
|
||||
// The output does not fit WithDecoderMaxMemory, which caps the total
|
||||
// decoded size when decoding in memory. A Reader follows the
|
||||
// streaming contract, where the option caps the window instead, so
|
||||
// decode this input as a stream after all; the shortcut has not
|
||||
// consumed the reader. Drop the oversized buffer rather than keep it
|
||||
// for reuse.
|
||||
if debugDecoder {
|
||||
println("sync decode to", len(dst), "bytes, err:", err)
|
||||
println("sync decode exceeded max memory after", len(dst), "bytes; streaming instead")
|
||||
}
|
||||
return nil
|
||||
d.syncStream.dstBuf = nil
|
||||
}
|
||||
// Remove current block.
|
||||
d.stashDecoder()
|
||||
@@ -217,14 +241,16 @@ func (d *Decoder) Reset(r io.Reader) error {
|
||||
// Ensure no-one else is still running...
|
||||
d.streamWg.Wait()
|
||||
if d.frame == nil {
|
||||
d.frame = newFrameDec(d.o)
|
||||
d.frame = newFrameDec(o)
|
||||
} else {
|
||||
d.frame.setOptions(o)
|
||||
}
|
||||
|
||||
if d.o.concurrent == 1 {
|
||||
if o.concurrent == 1 {
|
||||
return d.startSyncDecoder(r)
|
||||
}
|
||||
|
||||
d.current.output = make(chan decodeOutput, d.o.concurrent)
|
||||
d.current.output = make(chan decodeOutput, o.concurrent)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
d.current.cancel = cancel
|
||||
d.streamWg.Add(1)
|
||||
@@ -238,13 +264,21 @@ func (d *Decoder) Reset(r io.Reader) error {
|
||||
// Options are applied on top of the existing options.
|
||||
// Some options cannot be changed on reset and will return an error.
|
||||
func (d *Decoder) ResetWithOptions(r io.Reader, opts ...DOption) error {
|
||||
d.o.resetOpt = true
|
||||
defer func() { d.o.resetOpt = false }()
|
||||
if d.current.err == ErrDecoderClosed {
|
||||
return d.current.err
|
||||
}
|
||||
next := *d.publishedOptions.Load()
|
||||
next.dicts = maps.Clone(next.dicts)
|
||||
next.resetOpt = true
|
||||
for _, o := range opts {
|
||||
if err := o(&d.o); err != nil {
|
||||
if err := o(&next); err != nil {
|
||||
next.resetOpt = false
|
||||
d.publishedOptions.Store(&next)
|
||||
return err
|
||||
}
|
||||
}
|
||||
next.resetOpt = false
|
||||
d.publishedOptions.Store(&next)
|
||||
return d.Reset(r)
|
||||
}
|
||||
|
||||
@@ -324,6 +358,8 @@ func (d *Decoder) DecodeAll(input, dst []byte) ([]byte, error) {
|
||||
// Grab a block decoder and frame decoder.
|
||||
block := <-d.decoders
|
||||
frame := block.localFrame
|
||||
o := *d.publishedOptions.Load()
|
||||
frame.setOptions(o)
|
||||
initialSize := len(dst)
|
||||
defer func() {
|
||||
if debugDecoder {
|
||||
@@ -335,6 +371,7 @@ func (d *Decoder) DecodeAll(input, dst []byte) ([]byte, error) {
|
||||
frame.history.decoders.br.in = nil
|
||||
frame.history.decoders.br.cursor = 0
|
||||
}
|
||||
frame.o.dicts = nil
|
||||
d.decoders <- block
|
||||
}()
|
||||
frame.bBuf = input
|
||||
@@ -354,20 +391,20 @@ func (d *Decoder) DecodeAll(input, dst []byte) ([]byte, error) {
|
||||
if err = d.setDict(frame); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if frame.WindowSize > d.o.maxWindowSize {
|
||||
if frame.WindowSize > frame.o.maxWindowSize {
|
||||
if debugDecoder {
|
||||
println("window size exceeded:", frame.WindowSize, ">", d.o.maxWindowSize)
|
||||
println("window size exceeded:", frame.WindowSize, ">", frame.o.maxWindowSize)
|
||||
}
|
||||
return dst, ErrWindowSizeExceeded
|
||||
}
|
||||
if frame.FrameContentSize != fcsUnknown {
|
||||
if frame.FrameContentSize > d.o.maxDecodedSize-uint64(len(dst)-initialSize) {
|
||||
if frame.FrameContentSize > frame.o.maxDecodedSize-uint64(len(dst)-initialSize) {
|
||||
if debugDecoder {
|
||||
println("decoder size exceeded; fcs:", frame.FrameContentSize, "> mcs:", d.o.maxDecodedSize-uint64(len(dst)-initialSize), "len:", len(dst))
|
||||
println("decoder size exceeded; fcs:", frame.FrameContentSize, "> mcs:", frame.o.maxDecodedSize-uint64(len(dst)-initialSize), "len:", len(dst))
|
||||
}
|
||||
return dst, ErrDecoderSizeExceeded
|
||||
}
|
||||
if d.o.limitToCap && frame.FrameContentSize > uint64(cap(dst)-len(dst)) {
|
||||
if frame.o.limitToCap && frame.FrameContentSize > uint64(cap(dst)-len(dst)) {
|
||||
if debugDecoder {
|
||||
println("decoder size exceeded; fcs:", frame.FrameContentSize, "> (cap-len)", cap(dst)-len(dst))
|
||||
}
|
||||
@@ -380,14 +417,14 @@ func (d *Decoder) DecodeAll(input, dst []byte) ([]byte, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if cap(dst) == 0 && !d.o.limitToCap {
|
||||
if cap(dst) == 0 && !frame.o.limitToCap {
|
||||
// Allocate len(input) * 2 by default if nothing is provided
|
||||
// and we didn't get frame content size.
|
||||
size := min(
|
||||
// Cap to 1 MB.
|
||||
len(input)*2, 1<<20)
|
||||
if uint64(size) > d.o.maxDecodedSize {
|
||||
size = int(d.o.maxDecodedSize)
|
||||
if uint64(size) > frame.o.maxDecodedSize {
|
||||
size = int(frame.o.maxDecodedSize)
|
||||
}
|
||||
dst = make([]byte, 0, size)
|
||||
}
|
||||
@@ -396,7 +433,7 @@ func (d *Decoder) DecodeAll(input, dst []byte) ([]byte, error) {
|
||||
if err != nil {
|
||||
return dst, err
|
||||
}
|
||||
if uint64(len(dst)-initialSize) > d.o.maxDecodedSize {
|
||||
if uint64(len(dst)-initialSize) > frame.o.maxDecodedSize {
|
||||
return dst, ErrDecoderSizeExceeded
|
||||
}
|
||||
if len(frame.bBuf) == 0 {
|
||||
@@ -459,7 +496,7 @@ func (d *Decoder) nextBlock(blocking bool) (ok bool) {
|
||||
println("got", len(d.current.b), "bytes, error:", d.current.err, "data crc:", tmp)
|
||||
}
|
||||
|
||||
if d.o.ignoreChecksum {
|
||||
if d.frame.o.ignoreChecksum {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -497,7 +534,7 @@ func (d *Decoder) nextBlockSync() (ok bool) {
|
||||
if d.current.err != nil {
|
||||
return false
|
||||
}
|
||||
if d.frame.WindowSize > d.o.maxDecodedSize || d.frame.WindowSize > d.o.maxWindowSize {
|
||||
if d.frame.WindowSize > d.frame.o.maxDecodedSize || d.frame.WindowSize > d.frame.o.maxWindowSize {
|
||||
d.current.err = ErrDecoderSizeExceeded
|
||||
return false
|
||||
}
|
||||
@@ -546,11 +583,11 @@ func (d *Decoder) nextBlockSync() (ok bool) {
|
||||
|
||||
// Update/Check CRC
|
||||
if d.frame.HasCheckSum {
|
||||
if !d.o.ignoreChecksum {
|
||||
if !d.frame.o.ignoreChecksum {
|
||||
d.frame.crc.Write(d.current.b)
|
||||
}
|
||||
if d.current.d.Last {
|
||||
if !d.o.ignoreChecksum {
|
||||
if !d.frame.o.ignoreChecksum {
|
||||
d.current.err = d.frame.checkCRC()
|
||||
} else {
|
||||
d.current.err = d.frame.consumeCRC()
|
||||
@@ -656,8 +693,8 @@ func (d *Decoder) startStreamDecoder(ctx context.Context, r io.Reader, output ch
|
||||
defer d.streamWg.Done()
|
||||
br := readerWrapper{r: r}
|
||||
|
||||
var seqDecode = make(chan *blockDec, d.o.concurrent)
|
||||
var seqExecute = make(chan *blockDec, d.o.concurrent)
|
||||
var seqDecode = make(chan *blockDec, d.frame.o.concurrent)
|
||||
var seqExecute = make(chan *blockDec, d.frame.o.concurrent)
|
||||
|
||||
// Async 1: Decode sequences...
|
||||
go func() {
|
||||
@@ -858,9 +895,9 @@ decodeStream:
|
||||
if err == nil {
|
||||
err = d.setDict(frame)
|
||||
}
|
||||
if err == nil && d.frame.WindowSize > d.o.maxWindowSize {
|
||||
if err == nil && d.frame.WindowSize > d.frame.o.maxWindowSize {
|
||||
if debugDecoder {
|
||||
println("decoder size exceeded, fws:", d.frame.WindowSize, "> mws:", d.o.maxWindowSize)
|
||||
println("decoder size exceeded, fws:", d.frame.WindowSize, "> mws:", d.frame.o.maxWindowSize)
|
||||
}
|
||||
|
||||
err = ErrDecoderSizeExceeded
|
||||
@@ -940,7 +977,7 @@ decodeStream:
|
||||
}
|
||||
|
||||
func (d *Decoder) setDict(frame *frameDec) (err error) {
|
||||
dict, ok := d.o.dicts[frame.DictionaryID]
|
||||
dict, ok := frame.o.dicts[frame.DictionaryID]
|
||||
if ok {
|
||||
if debugDecoder {
|
||||
println("setting dict", frame.DictionaryID)
|
||||
|
||||
+7
-4
@@ -49,13 +49,16 @@ const (
|
||||
)
|
||||
|
||||
func newFrameDec(o decoderOptions) *frameDec {
|
||||
d := frameDec{}
|
||||
d.setOptions(o)
|
||||
return &d
|
||||
}
|
||||
|
||||
func (d *frameDec) setOptions(o decoderOptions) {
|
||||
if o.maxWindowSize > o.maxDecodedSize {
|
||||
o.maxWindowSize = o.maxDecodedSize
|
||||
}
|
||||
d := frameDec{
|
||||
o: o,
|
||||
}
|
||||
return &d
|
||||
d.o = o
|
||||
}
|
||||
|
||||
// reset will read the frame header and prepare for block decoding.
|
||||
|
||||
+13
-22
@@ -1,7 +1,7 @@
|
||||
// Code generated by command: go run gen_fse.go -out ../fse_decoder.s -arch amd64,arm64 -pkg=zstd. DO NOT EDIT.
|
||||
// EXPERIMENTAL arm64 output lowered from an amd64 avo program.
|
||||
|
||||
//go:build arm64 && !appengine && !noasm && gc && !noasm
|
||||
//go:build arm64 && (!appengine && !noasm && gc && !noasm)
|
||||
|
||||
// func buildDtable_asm(s *fseDecoder, ctx *buildDtableAsmContext) int
|
||||
TEXT ·buildDtable_asm(SB), $0-24
|
||||
@@ -26,11 +26,10 @@ TEXT ·buildDtable_asm(SB), $0-24
|
||||
JMP init_main_loop_condition
|
||||
|
||||
init_main_loop:
|
||||
ADD R8<<1, R1, R15
|
||||
MOVH (R15), R9
|
||||
AND $0xffff, R9, R15
|
||||
MOVH (R1)(R8<<1), R9
|
||||
MOVD $-1, R16
|
||||
AND $0xffff, R16, R16
|
||||
AND $0xffff, R9, R15
|
||||
CMP R16, R15
|
||||
BNE do_not_update_high_threshold
|
||||
ADD R7<<3, R5, R15
|
||||
@@ -39,8 +38,7 @@ init_main_loop:
|
||||
MOVD $0x0000000000000001, R9
|
||||
|
||||
do_not_update_high_threshold:
|
||||
ADD R8<<1, R3, R15
|
||||
MOVH R9, (R15)
|
||||
MOVH R9, (R3)(R8<<1)
|
||||
ADD $1, R8, R8
|
||||
|
||||
init_main_loop_condition:
|
||||
@@ -49,12 +47,10 @@ init_main_loop_condition:
|
||||
|
||||
// Spread symbols
|
||||
// Calculate table step
|
||||
MOVD R0, R8
|
||||
LSR $0x01, R8, R8
|
||||
MOVD R0, R9
|
||||
LSR $0x03, R9, R9
|
||||
ADD R9, R8, R8
|
||||
ADD $3, R8, R8
|
||||
LSR $0x01, R0, R8
|
||||
LSR $0x03, R0, R9
|
||||
ADD R9, R8, R8
|
||||
ADD $3, R8, R8
|
||||
|
||||
// Fill add bits values
|
||||
SUB $1, R0, R9
|
||||
@@ -64,8 +60,7 @@ init_main_loop_condition:
|
||||
|
||||
spread_main_loop:
|
||||
MOVD $0, R12
|
||||
ADD R11<<1, R1, R15
|
||||
MOVH (R15), R13
|
||||
MOVH (R1)(R11<<1), R13
|
||||
JMP spread_inner_loop_condition
|
||||
|
||||
spread_inner_loop:
|
||||
@@ -102,11 +97,9 @@ spread_check_ok:
|
||||
build_table_main_table:
|
||||
ADD R6<<3, R5, R15
|
||||
MOVBU 1(R15), R1
|
||||
ADD R1<<1, R3, R15
|
||||
MOVHU (R15), R7
|
||||
MOVHU (R3)(R1<<1), R7
|
||||
ADD $1, R7, R8
|
||||
ADD R1<<1, R3, R15
|
||||
MOVH R8, (R15)
|
||||
MOVH R8, (R3)(R1<<1)
|
||||
MOVD R7, R8
|
||||
CLZ R8, R16
|
||||
MOVD $63, R8
|
||||
@@ -129,12 +122,10 @@ build_table_main_table:
|
||||
RET
|
||||
|
||||
build_table_check1_ok:
|
||||
AND $0xff, R1, R15
|
||||
AND $0xff, R1, R16
|
||||
TST R16, R15
|
||||
TST $0xff, R1
|
||||
BNE build_table_check2_ok
|
||||
AND $0xffff, R7, R15
|
||||
AND $0xffff, R6, R16
|
||||
AND $0xffff, R7, R15
|
||||
CMP R16, R15
|
||||
BNE build_table_check2_ok
|
||||
MOVD ctx+8(FP), R0
|
||||
|
||||
+12
@@ -52,6 +52,11 @@ func (d *Digest) Reset() {
|
||||
d.n = 0
|
||||
}
|
||||
|
||||
// maxAsmSize bounds the input handed to a single writeBlocks call. Assembly is
|
||||
// never preemptible, so an unbounded call holds every P in stop-the-world for
|
||||
// its duration. Whole 32-byte blocks, so Write can feed it in pieces.
|
||||
const maxAsmSize = 128 << 10 // 4096 whole 32-byte blocks
|
||||
|
||||
// Size always returns 8 bytes.
|
||||
func (d *Digest) Size() int { return 8 }
|
||||
|
||||
@@ -83,6 +88,13 @@ func (d *Digest) Write(b []byte) (n int, err error) {
|
||||
d.n = 0
|
||||
}
|
||||
|
||||
for len(b) >= maxAsmSize {
|
||||
// Assembly is not preemptible, so hashing a large buffer in one call
|
||||
// blocks every stop-the-world for the whole call. Feed it in chunks.
|
||||
writeBlocks(d, b[:maxAsmSize])
|
||||
b = b[maxAsmSize:]
|
||||
}
|
||||
|
||||
if len(b) >= 32 {
|
||||
// One or more full blocks left.
|
||||
nw := writeBlocks(d, b)
|
||||
|
||||
+5
-1
@@ -175,7 +175,11 @@ finalize:
|
||||
RET
|
||||
|
||||
// func writeBlocks(d *Digest, b []byte) int
|
||||
TEXT ·writeBlocks(SB), NOSPLIT|NOFRAME, $0-40
|
||||
//
|
||||
// Deliberately not NOSPLIT: the stack check is the preemption point that lets a
|
||||
// stop-the-world proceed between chunks. The frame must be >= abi.StackSmall
|
||||
// (128), or the assembler marks this leaf NOSPLIT anyway and drops that check.
|
||||
TEXT ·writeBlocks(SB), $128-40
|
||||
// Load fixed primes needed for round.
|
||||
MOVQ ·primes+0(SB), prime1
|
||||
MOVQ ·primes+8(SB), prime2
|
||||
|
||||
+5
-1
@@ -163,7 +163,11 @@ finalize:
|
||||
RET
|
||||
|
||||
// func writeBlocks(s *Digest, b []byte) int
|
||||
TEXT ·writeBlocks(SB), NOSPLIT|NOFRAME, $0-40
|
||||
//
|
||||
// Deliberately not NOSPLIT: the stack check is the preemption point that lets a
|
||||
// stop-the-world proceed between chunks. The frame must be >= abi.StackSmall
|
||||
// (128), or the assembler marks this leaf NOSPLIT anyway and drops that check.
|
||||
TEXT ·writeBlocks(SB), $128-40
|
||||
LDP ·primes+0(SB), (prime1, prime2)
|
||||
|
||||
// Load state. Assume v[1-4] are stored contiguously.
|
||||
|
||||
+23
@@ -99,6 +99,29 @@ func (s *sequenceDecs) initialize(br *bitReader, hist *history, out []byte) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
// consumeSyncLen accounts for n bytes of frame output produced outside
|
||||
// decodeSync. maxSyncLen bounds how much the frame may still produce and
|
||||
// decodeSyncSimple compares it with the output buffer's slack to decide
|
||||
// whether the extended 16-byte copies are safe. Blocks with sequences
|
||||
// subtracted their output; raw blocks, RLE blocks and compressed blocks
|
||||
// holding only literals did not, so a frame that opened with a raw block
|
||||
// overstated its remaining size for every block after it and ran the
|
||||
// bounds-exact copies for the rest of the frame.
|
||||
//
|
||||
// Zero means "no bound" and selects the conservative paths, so a block that
|
||||
// meets or exceeds the bound lands there; the frame-size checks in
|
||||
// runDecoder reject the excess afterwards.
|
||||
func (s *sequenceDecs) consumeSyncLen(n int) {
|
||||
if s.maxSyncLen == 0 {
|
||||
return
|
||||
}
|
||||
if uint64(n) >= s.maxSyncLen {
|
||||
s.maxSyncLen = 0
|
||||
return
|
||||
}
|
||||
s.maxSyncLen -= uint64(n)
|
||||
}
|
||||
|
||||
func (s *sequenceDecs) freeDecoders() {
|
||||
if f := s.litLengths.fse; f != nil && !f.preDefined {
|
||||
fseDecoderPool.Put(f)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user