Merge tag 'v29.9.0' into 7417-29.x

v29.9.0

# -----BEGIN PGP SIGNATURE-----
#
# iK0EABYKAFUWIQQX90wwlHYgZQhz61ZuYvmhLGnMZgUCasf+dhsUgAAAAAAEAA5t
# YW51MiwyLjUrMS4xMiwwLDMbHHBhd2VsLmdyb25vd3NraUBkb2NrZXIuY29tAAoJ
# EG5i+aEsacxm3EYBAKRhamCCxqMBniTJ9GtLRVn7G3gbweSztHdAzhCo70C6AP41
# K+a4eqECp+bjqTK1ldegnmdyAPZ8g8AoPMpJgSQ2CA==
# =N5Qp
# -----END PGP SIGNATURE-----
# gpg: Signature made Thu Oct  8 22:35:02 2026 CEST
# gpg:                using EDDSA key 17F74C30947620650873EB566E62F9A12C69CC66
# gpg:                issuer "pawel.gronowski@docker.com"
# gpg: Good signature from "Paweł Gronowski <pawel.gronowski@docker.com>" [ultimate]
This commit is contained in:
Paweł Gronowski
2026-10-20 12:39:56 +02:00
146 changed files with 10525 additions and 4421 deletions
+4 -4
View File
@@ -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.9"
cache: false
-
name: Initialize CodeQL
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
languages: go
-
name: Autobuild
uses: github/codeql-action/autobuild@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
uses: github/codeql-action/autobuild@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
-
name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
category: "/language:go"
+1
View File
@@ -37,6 +37,7 @@ jobs:
- alpine
- debian
engine-version:
- master # moby/moby-bin:master
- rc # latest rc
- 29 # latest
- 28 # latest - 1
+1 -1
View File
@@ -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.9"
cache: false
-
name: Test
+1 -1
View File
@@ -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.9"
cache: false
-
name: Run gocompat check
+1 -1
View File
@@ -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.9"
timeout: 5m
+1 -1
View File
@@ -8,7 +8,7 @@ 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.9
# 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.
+1 -1
View File
@@ -1 +1 @@
29.8.2
29.9.0
+6
View File
@@ -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"`
}
+13
View File
@@ -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)
+13
View File
@@ -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 {
+58
View File
@@ -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
+10 -10
View File
@@ -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
View File
@@ -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=
+322
View File
@@ -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
}
+682
View File
@@ -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)
}
+17 -1
View File
@@ -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
+1 -1
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.26.8
ARG GO_VERSION=1.26.9
# 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 -1
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.26.8
ARG GO_VERSION=1.26.9
# 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 -1
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.26.8
ARG GO_VERSION=1.26.9
# 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
View File
@@ -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:
+1 -1
View File
@@ -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
View File
@@ -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 && \
+10 -1
View File
@@ -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
+5
View File
@@ -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,
+9 -9
View File
@@ -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.29
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
@@ -70,8 +70,8 @@ require (
)
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-0.20260930231756-f19d9717deb0 // indirect; see https://github.com/microsoft/hcsshim/pull/2545
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
@@ -87,7 +87,7 @@ 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.1 // indirect
@@ -102,7 +102,7 @@ require (
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.41.0 // indirect
golang.org/x/net v0.59.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
+18 -19
View File
@@ -2,10 +2,10 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
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-0.20260930231756-f19d9717deb0 h1:71VU8kW/LCxJkx41+YYys1EgQO+AUnPa1TrxTSyYUCU=
github.com/Microsoft/go-winio v0.6.3-0.20260930231756-f19d9717deb0/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=
@@ -18,8 +18,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/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.29 h1:3oGF3R/S2N9DQ3ptftzVIvg2eicmojCzlwBEmqEPDfQ=
github.com/mattn/go-runewidth v0.0.29/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=
@@ -193,8 +193,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-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.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
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=
@@ -203,7 +203,6 @@ 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.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
+3 -3
View File
@@ -26,7 +26,7 @@ func (csiState csiEntryState) Handle(b byte) (s state, e error) {
func (csiState csiEntryState) Transition(s state) error {
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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,7 +1,5 @@
package ansiterm
type stateID int
type state interface {
Enter() error
Exit() error
-10
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+9 -5
View File
@@ -1,5 +1,4 @@
//go:build windows
// +build windows
package winio
@@ -235,12 +234,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
View File
@@ -1,5 +1,4 @@
//go:build windows
// +build windows
package winio
-1
View File
@@ -1,5 +1,4 @@
//go:build windows
// +build windows
package winio
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
},
+21 -19
View File
@@ -1,5 +1,4 @@
//go:build windows
// +build windows
package winio
@@ -11,6 +10,7 @@ import (
"net"
"os"
"runtime"
"sync"
"time"
"unsafe"
@@ -179,14 +179,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 +317,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 +346,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 +447,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 = ErrPipeListenerClosed
}
return p, err
}
@@ -530,8 +533,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
@@ -573,11 +576,10 @@ func (l *win32PipeListener) Accept() (net.Conn, error) {
}
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
View File
@@ -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
View File
@@ -1,5 +1,4 @@
//go:build !windows
// +build !windows
package guid
-1
View File
@@ -1,5 +1,4 @@
//go:build windows
// +build windows
package guid
+3 -2
View File
@@ -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
View File
@@ -1,5 +1,4 @@
//go:build windows
// +build windows
package winio
+55 -6
View File
@@ -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
View File
@@ -1,5 +1,4 @@
//go:build windows
// +build windows
package winio
+2 -6
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
* text=auto eol=lf
+35 -15
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
}
File diff suppressed because it is too large Load Diff
+39 -4
View File
@@ -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)
}
File diff suppressed because it is too large Load Diff
+79 -19
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+1062 -1023
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -83,9 +83,10 @@ const errorOverread = 6
// When the output and literal buffers have compressedBlockOverAlloc (16)
// bytes of slack past their logical use, the assembly may use extended
// memory copies that read and write in 16-byte blocks, overrunning the end
// of a literal run or match by up to 15 bytes. Otherwise it must use the
// bounds-exact ("safe") copies. This mirrors the analogous, always-dynamic
// selection in executeSimple below.
// of a literal run or match by up to 15 bytes; an empty literal run still
// copies one block, reading and writing the full 16 bytes of slack past
// its position. Otherwise it must use the bounds-exact ("safe") copies.
// This mirrors the analogous, always-dynamic selection in executeSimple below.
//
// The unsafe copies were disabled in #644 (2022) as a mitigation for a
// crash, but that crash's root cause — an unguarded bitReader overread that
+5
View File
@@ -15,6 +15,11 @@ Usage
runewidth.StringWidth("つのだ☆HIRO") == 12
```
NOTE: Latin letters that Unicode gives ambiguous East Asian width, such as
`ü`, `é` and `ß`, are measured as one cell even when `EastAsianWidth` is
set, as terminals draw them in CJK locales too. Other ambiguous characters
such as `±`, `×` and `○` still take two cells there.
Author
------
+342 -28
View File
@@ -54,10 +54,24 @@ var (
strictWidthLUT [2][0x110000]byte
strictWidthLUTLimit atomic.Int32
strictWidthLUTOnce sync.Once
// joinerBits is the joiner table as a bitmap over
// [joinerBase, 0x10000), the range where the per-rune test has to be
// cheap for the fast paths in StringWidth and Wrap to pay off. It is
// 7.6 KB and init fills it in a few microseconds. Runes above it stay
// on the interval search: they are rare in the text these fast paths
// are for, and the emoji that are common there leave the fast path at
// their first joiner anyway.
joinerBits [(0x10000 - joinerBase) / 8]byte
)
// joinerBase is the lowest rune in the joiner table. CR is the only rune
// below it that can form a multi-rune cluster, by pairing with LF.
const joinerBase = 0x300
func init() {
initStrictWidthLUTLow()
fillJoinerBits()
strictWidthLUTLimit.Store(0x300)
handleEnv()
}
@@ -347,9 +361,11 @@ var private = table{
var nonprint = table{
{0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD},
{0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F},
{0x2028, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF},
{0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF},
{0x061C, 0x061C}, {0x070F, 0x070F}, {0x180B, 0x180E},
{0x200B, 0x200F}, {0x2028, 0x202E}, {0x2060, 0x2064},
{0x2066, 0x206F}, {0xD800, 0xDFFF}, {0xFEFF, 0xFEFF},
{0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF}, {0x13430, 0x1343F},
{0x1BCA0, 0x1BCA3}, {0x1D173, 0x1D17A}, {0xE0001, 0xE0001},
}
// Condition have flag EastAsianWidth whether the current locale is CJK or not.
@@ -446,19 +462,131 @@ func (c *Condition) CreateLUT() {
c.lutStrictEmojiNeutral = c.StrictEmojiNeutral
}
// isASCII reports whether s has no byte above 0x7F, in which case every
// grapheme cluster in s is a single byte apart from CRLF.
func isASCII(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] >= 0x80 {
return false
}
}
return true
}
// fillJoinerBits paints joinerBits from the joiner table.
func fillJoinerBits() {
for _, iv := range joiner {
if iv.first >= 0x10000 {
break // the table is sorted, the rest is astral
}
lo, hi := int(iv.first)-joinerBase, int(iv.last)-joinerBase
if hi >= len(joinerBits)*8 {
hi = len(joinerBits)*8 - 1
}
for lo <= hi {
if lo&7 == 0 && lo+7 <= hi {
joinerBits[lo>>3] = 0xFF
lo += 8
continue
}
joinerBits[lo>>3] |= 1 << uint(lo&7)
lo++
}
}
// A byte that is not valid UTF-8 decodes to U+FFFD, and the segmenter
// can gather a run of such bytes into one cluster, which a rune loop
// has no way to see. Marking U+FFFD keeps those strings on the
// segmenter, and the only string it holds back needlessly is one that
// really contains U+FFFD.
i := int(utf8.RuneError) - joinerBase
joinerBits[i>>3] |= 1 << uint(i&7)
}
// isJoiner reports whether r can join with a neighbour into a multi-rune
// grapheme cluster. A string of runes for which it reports false has one
// rune per cluster, so measuring it needs no grapheme segmentation.
func isJoiner(r rune) bool {
if r < joinerBase {
return r == '\r'
}
if r < 0x10000 {
i := r - joinerBase
return joinerBits[i>>3]&(1<<(uint(i)&7)) != 0
}
return inTable(r, joiner)
}
// clusterText returns s to segment in place of s. When s ends in a lead
// byte short of the bytes it promises, the segmenter gives up there and
// glues everything from it onto the cluster before, where the two-cell cap
// hides it, although the same bytes followed by anything else form
// clusters of their own. Such a string gets NULs after it, enough for any
// lead byte to be decoded; as controls they are clusters of their own, and
// the loops over the clusters stop at len(s) before reaching them.
func clusterText(s string) string {
if endsShort(s) {
return s + "\x00\x00\x00"
}
return s
}
// endsShort reports whether one of the last three bytes of s is a lead
// byte that needs more bytes than are left, which is where the segmenter
// stops decoding. It only looks at the length the lead byte announces, not
// at whether the bytes after it could continue it, as the segmenter does.
func endsShort(s string) bool {
for i := max(len(s)-3, 0); i < len(s); i++ {
b, left := s[i], len(s)-i
if b >= 0xC2 && b < 0xE0 && left < 2 ||
b >= 0xE0 && b < 0xF0 && left < 3 ||
b >= 0xF0 && b < 0xF8 && left < 4 {
return true
}
}
return false
}
// graphemeWidth returns the width of a single grapheme cluster: the sum of
// the widths of its runes, capped at 2 cells. The cap keeps multi-rune
// sequences that render as a single glyph (ZWJ emoji, flags, Hangul jamo)
// from being counted wider than the two cells terminals give them.
// the widths of its runes, with the part from the first rune that starts a
// single glyph (an emoji, a regional indicator, a Hangul jamo or syllable)
// capped at 2 cells. The cap keeps multi-rune sequences that render as one
// glyph (ZWJ emoji, skin tones, flags, Hangul jamo) from being counted
// wider than the two cells terminals give them. What comes before it, such
// as a Prepend rune, and a cluster with no such rune, like a letter with a
// skin tone modifier after it, are shown rune by rune and summed.
func (c *Condition) graphemeWidth(cluster string) int {
width := 0
if r, size := utf8.DecodeRuneInString(cluster); size == len(cluster) {
return c.RuneWidth(r) // one rune, never over the cap
}
width, glyph := 0, -1
for _, r := range cluster {
width += c.RuneWidth(r)
w := c.RuneWidth(r)
if glyph < 0 && r >= 0x1100 && startsGlyph(r) {
glyph = 0
}
if glyph >= 0 {
glyph += w
} else {
width += w
}
}
if width > 2 {
width = 2
return width + min(max(glyph, 0), 2)
}
// startsGlyph reports whether r begins a sequence that a terminal draws as
// a single glyph however many runes it has.
func startsGlyph(r rune) bool {
switch {
case 0x1100 <= r && r <= 0x11FF, // Hangul jamo
0xA960 <= r && r <= 0xA97F, // Hangul jamo extended-A
0xAC00 <= r && r <= 0xD7FF, // Hangul syllables, jamo extended-B
0x1F1E6 <= r && r <= 0x1F1FF: // regional indicators
return true
case r < 0x203C, 0x3299 < r && r < 0x1F004:
// Outside the emoji table: spares CJK text the search.
return false
}
return width
return inTable(r, emoji)
}
// StringWidth return width as you can see
@@ -468,6 +596,11 @@ func (c *Condition) StringWidth(s string) (width int) {
if b < 0x20 || b == 0x7F {
return 0
}
if b >= 0x80 {
// Not UTF-8 on its own: measure it as the U+FFFD every other
// path decodes it to, which is ambiguous width.
return c.RuneWidth(utf8.RuneError)
}
return 1
}
if len(s) > 0 && len(s) <= utf8.UTFMax {
@@ -489,24 +622,48 @@ func (c *Condition) StringWidth(s string) (width int) {
return
graphemes:
// Runes first: until one of them can join a cluster, each cluster is a
// single rune and segmenting the string would only find that out the
// expensive way.
if w, ok := c.runeWidthSum(s); ok {
width = w
return
}
width = 0
g := graphemes.FromString(s)
for g.Next() {
g := graphemes.FromString(clusterText(s))
for g.Next() && g.Start() < len(s) {
width += c.graphemeWidth(g.Value())
}
return
}
// runeWidthSum adds up the widths of the runes in s, and reports false
// without a total once it meets a rune that can join a cluster, which is
// where per-rune widths stop being the whole story.
func (c *Condition) runeWidthSum(s string) (int, bool) {
width := 0
for _, r := range s {
if isJoiner(r) {
return 0, false
}
width += c.RuneWidth(r)
}
return width, true
}
// Truncate return string truncated with w cells
func (c *Condition) Truncate(s string, w int, tail string) string {
if c.StringWidth(s) <= w {
return s
}
w -= c.StringWidth(tail)
if pos, ok := c.truncateRunes(s, w); ok {
return s[:pos] + tail
}
var width int
pos := len(s)
g := graphemes.FromString(s)
for g.Next() {
g := graphemes.FromString(clusterText(s))
for g.Next() && g.Start() < len(s) {
chWidth := c.graphemeWidth(g.Value())
if width+chWidth > w {
pos = g.Start()
@@ -517,17 +674,50 @@ func (c *Condition) Truncate(s string, w int, tail string) string {
return s[:pos] + tail
}
// truncateRunes is the loop in Truncate with every rune taken for a whole
// cluster, which holds until a rune can join one. It reports false there
// and leaves the string to the segmenter.
func (c *Condition) truncateRunes(s string, w int) (int, bool) {
width := 0
for i, r := range s {
if isJoiner(r) {
return 0, false
}
cw := c.RuneWidth(r)
if width+cw > w {
return i, true
}
width += cw
}
return len(s), true
}
// endsCluster reports whether the byte at i, which follows a rune that
// cannot join a cluster, also starts one. Cutting there is only safe when
// the rune that follows does not reach back.
func endsCluster(s string, i int) bool {
if i >= len(s) {
return true
}
r, _ := utf8.DecodeRuneInString(s[i:])
return !isJoiner(r)
}
// TruncateLeft cuts w cells from the beginning of the `s`.
func (c *Condition) TruncateLeft(s string, w int, prefix string) string {
if c.StringWidth(s) <= w {
return prefix
}
if pos, pad, ok := c.truncateLeftRunes(s, w); ok {
return prefix + strings.Repeat(" ", pad) + s[pos:]
}
var width int
pos := len(s)
g := graphemes.FromString(s)
for g.Next() {
g := graphemes.FromString(clusterText(s))
for g.Next() && g.Start() < len(s) {
chWidth := c.graphemeWidth(g.Value())
if width+chWidth > w {
@@ -547,6 +737,32 @@ func (c *Condition) TruncateLeft(s string, w int, prefix string) string {
return prefix + s[pos:]
}
// truncateLeftRunes is the loop in TruncateLeft with every rune taken for a
// whole cluster, returning the cut and the padding that replaces the cell
// the cut lands inside. It reports false at the first rune that can join a
// cluster.
func (c *Condition) truncateLeftRunes(s string, w int) (pos, pad int, ok bool) {
width := 0
for i, r := range s {
if isJoiner(r) {
return 0, 0, false
}
cw := c.RuneWidth(r)
if width+cw > w {
if width >= w {
return i, 0, true
}
end := i + utf8.RuneLen(r)
if !endsCluster(s, end) {
return 0, 0, false
}
return end, width + cw - w, true
}
width += cw
}
return len(s), 0, true
}
// TruncatePrefix cuts the beginning of `s` so the result fits in w cells, with prefix prepended
func (c *Condition) TruncatePrefix(s string, w int, prefix string) string {
if c.StringWidth(prefix) >= w {
@@ -558,10 +774,13 @@ func (c *Condition) TruncatePrefix(s string, w int, prefix string) string {
return s
}
w -= c.StringWidth(prefix)
if pos, ok := c.truncatePrefixRunes(s, sw, w); ok {
return prefix + s[pos:]
}
var width int
var pos int
g := graphemes.FromString(s)
for g.Next() {
g := graphemes.FromString(clusterText(s))
for g.Next() && g.Start() < len(s) {
chWidth := c.graphemeWidth(g.Value())
if sw-(width+chWidth) <= w {
pos = g.End()
@@ -573,29 +792,119 @@ func (c *Condition) TruncatePrefix(s string, w int, prefix string) string {
return prefix + s[pos:]
}
// Wrap return string wrapped with w cells
func (c *Condition) Wrap(s string, w int) string {
// truncatePrefixRunes is the loop in TruncatePrefix with every rune taken
// for a whole cluster, reporting false at the first rune that can join one.
func (c *Condition) truncatePrefixRunes(s string, sw, w int) (int, bool) {
width := 0
for i, r := range s {
if isJoiner(r) {
return 0, false
}
cw := c.RuneWidth(r)
if sw-(width+cw) <= w {
end := i + utf8.RuneLen(r)
if !endsCluster(s, end) {
return 0, false
}
return end, true
}
width += cw
}
return 0, true
}
// wrapRunes wraps s treating every rune as its own grapheme cluster, and
// reports false without a result once it meets a rune that can join one.
func (c *Condition) wrapRunes(s string, w int) (string, bool) {
width := 0
var out strings.Builder
// max keeps the capacity hint from dividing by zero when w is 0; a
// non-positive width breaks before every rune, as it always has.
out.Grow(len(s) + len(s)/max(w, 1) + 1)
for _, r := range s {
if isJoiner(r) {
return "", false
}
cw := c.RuneWidth(r)
if r == '\n' {
out.WriteRune(r)
width = 0
continue
} else if width+cw > w {
}
if width+cw > w {
out.WriteByte('\n')
width = 0
out.WriteRune(r)
width += cw
continue
}
out.WriteRune(r)
width += cw
}
return out.String(), true
}
// Wrap return string wrapped with w cells
func (c *Condition) Wrap(s string, w int) string {
// ASCII fast path: no grapheme clustering needed for pure ASCII
if isASCII(s) {
width := 0
var out strings.Builder
// max keeps the capacity hint from dividing by zero when w is 0;
// a non-positive width breaks before every cluster, as it always
// has.
out.Grow(len(s) + len(s)/max(w, 1) + 1)
for i := 0; i < len(s); i++ {
b := s[i]
if b == '\n' {
out.WriteByte(b)
width = 0
continue
}
// CRLF is one cluster that ends the line, as on the
// segmenter, so it is never broken before.
if b == '\r' && i+1 < len(s) && s[i+1] == '\n' {
out.WriteString("\r\n")
width = 0
i++
continue
}
// Same rule as the StringWidth fast path: no ASCII byte is
// wide or ambiguous, so the flags in c cannot change this.
cw := 0
if b >= 0x20 && b != 0x7F {
cw = 1
}
if width+cw > w {
out.WriteByte('\n')
width = 0
}
out.WriteByte(b)
width += cw
}
return out.String()
}
// Runes first, as in StringWidth. Reaching a rune that can join a
// cluster throws the wrapped text away and starts over on the
// segmenter, which is the uncommon case.
if out, ok := c.wrapRunes(s, w); ok {
return out
}
width := 0
var out strings.Builder
out.Grow(len(s) + len(s)/max(w, 1) + 1)
g := graphemes.FromString(clusterText(s))
for g.Next() && g.Start() < len(s) {
cluster := g.Value()
// LF and CRLF are each a single cluster
if strings.HasSuffix(cluster, "\n") {
out.WriteString(cluster)
width = 0
continue
}
cw := c.graphemeWidth(cluster)
if width+cw > w {
out.WriteByte('\n')
width = 0
}
out.WriteString(cluster)
width += cw
}
return out.String()
}
@@ -626,8 +935,13 @@ func RuneWidth(r rune) int {
}
// IsAmbiguousWidth returns whether is ambiguous width or not.
//
// NOTE: The Latin letters of U+0080..U+017F that Unicode gives ambiguous
// width (ü, é, ß, æ, ø and so on) are reported as ambiguous here, but
// RuneWidth measures them as one cell even with EastAsianWidth, since
// terminals draw them narrow in CJK locales too.
func IsAmbiguousWidth(r rune) bool {
return inTable(r, private) || inTable(r, ambiguous)
return inTable(r, private) || inTable(r, ambiguous) || inTable(r, ambiguousLatin)
}
// IsCombiningWidth returns whether is combining width or not.
+35 -8
View File
@@ -14,9 +14,11 @@ func mblen(charset string) int {
return 6
case "jis":
return 8
case "euctw", "gb18030":
return 4
case "eucjp":
return 3
case "euckr", "euccn", "sjis", "cp932", "cp51932", "cp936", "cp949", "cp950", "big5", "gbk", "gb2312":
case "euckr", "euccn", "sjis", "shiftjis", "cp932", "cp51932", "cp936", "cp949", "cp950", "big5", "big5hkscs", "gbk", "gb2312":
return 2
}
return 1
@@ -45,9 +47,35 @@ func localeCharset(locale string) string {
return ""
}
// isCJKLanguage reports whether the language code at the start of locale is
// Japanese, Korean or Chinese.
func isCJKLanguage(locale string) bool {
n := 0
for n < len(locale) && locale[n] >= 'a' && locale[n] <= 'z' {
n++
}
switch locale[:n] {
case "ja", "ko", "zh":
return true
}
return false
}
// normalizeCharset drops the hyphens and underscores that charset names are
// spelled with or without, so that "EUC-JP" and "eucjp" compare equal.
func normalizeCharset(charset string) string {
return strings.Map(func(r rune) rune {
if r == '-' || r == '_' {
return -1
}
return r
}, charset)
}
func isEastAsian(locale string) bool {
charset := strings.ToLower(locale)
if cs := localeCharset(locale); cs != "" {
cs := localeCharset(locale)
if cs != "" {
charset = strings.ToLower(cs)
}
@@ -61,14 +89,13 @@ func isEastAsian(locale string) bool {
break
}
}
max := mblen(charset)
if max > 1 && (charset[0] != 'u' ||
strings.HasPrefix(locale, "ja") ||
strings.HasPrefix(locale, "ko") ||
strings.HasPrefix(locale, "zh")) {
cjk := isCJKLanguage(locale)
if cs == "" && cjk {
// A CJK locale without a charset, such as "ja_JP" on macOS.
return true
}
return false
max := mblen(normalizeCharset(charset))
return max > 1 && (charset[0] != 'u' || cjk)
}
// IsEastAsian return true if the current locale is CJK
+175 -52
View File
@@ -173,65 +173,71 @@ var doublewidth = table{
var ambiguous = table{
{0x00A1, 0x00A1}, {0x00A4, 0x00A4}, {0x00A7, 0x00A8},
{0x00AA, 0x00AA}, {0x00AD, 0x00AE}, {0x00B0, 0x00B4},
{0x00B6, 0x00BA}, {0x00BC, 0x00BF}, {0x00C6, 0x00C6},
{0x00D0, 0x00D0}, {0x00D7, 0x00D8}, {0x00DE, 0x00E1},
{0x00AD, 0x00AE}, {0x00B0, 0x00B4}, {0x00B6, 0x00B9},
{0x00BC, 0x00BF}, {0x00D7, 0x00D7}, {0x00F7, 0x00F7},
{0x01CE, 0x01CE}, {0x01D0, 0x01D0}, {0x01D2, 0x01D2},
{0x01D4, 0x01D4}, {0x01D6, 0x01D6}, {0x01D8, 0x01D8},
{0x01DA, 0x01DA}, {0x01DC, 0x01DC}, {0x0251, 0x0251},
{0x0261, 0x0261}, {0x02C4, 0x02C4}, {0x02C7, 0x02C7},
{0x02C9, 0x02CB}, {0x02CD, 0x02CD}, {0x02D0, 0x02D0},
{0x02D8, 0x02DB}, {0x02DD, 0x02DD}, {0x02DF, 0x02DF},
{0x0300, 0x036F}, {0x0391, 0x03A1}, {0x03A3, 0x03A9},
{0x03B1, 0x03C1}, {0x03C3, 0x03C9}, {0x0401, 0x0401},
{0x0410, 0x044F}, {0x0451, 0x0451}, {0x2010, 0x2010},
{0x2013, 0x2016}, {0x2018, 0x2019}, {0x201C, 0x201D},
{0x2020, 0x2022}, {0x2024, 0x2027}, {0x2030, 0x2030},
{0x2032, 0x2033}, {0x2035, 0x2035}, {0x203B, 0x203B},
{0x203E, 0x203E}, {0x2074, 0x2074}, {0x207F, 0x207F},
{0x2081, 0x2084}, {0x20AC, 0x20AC}, {0x2103, 0x2103},
{0x2105, 0x2105}, {0x2109, 0x2109}, {0x2113, 0x2113},
{0x2116, 0x2116}, {0x2121, 0x2122}, {0x2126, 0x2126},
{0x212B, 0x212B}, {0x2153, 0x2154}, {0x215B, 0x215E},
{0x2160, 0x216B}, {0x2170, 0x2179}, {0x2189, 0x2189},
{0x2190, 0x2199}, {0x21B8, 0x21B9}, {0x21D2, 0x21D2},
{0x21D4, 0x21D4}, {0x21E7, 0x21E7}, {0x2200, 0x2200},
{0x2202, 0x2203}, {0x2207, 0x2208}, {0x220B, 0x220B},
{0x220F, 0x220F}, {0x2211, 0x2211}, {0x2215, 0x2215},
{0x221A, 0x221A}, {0x221D, 0x2220}, {0x2223, 0x2223},
{0x2225, 0x2225}, {0x2227, 0x222C}, {0x222E, 0x222E},
{0x2234, 0x2237}, {0x223C, 0x223D}, {0x2248, 0x2248},
{0x224C, 0x224C}, {0x2252, 0x2252}, {0x2260, 0x2261},
{0x2264, 0x2267}, {0x226A, 0x226B}, {0x226E, 0x226F},
{0x2282, 0x2283}, {0x2286, 0x2287}, {0x2295, 0x2295},
{0x2299, 0x2299}, {0x22A5, 0x22A5}, {0x22BF, 0x22BF},
{0x2312, 0x2312}, {0x2460, 0x24E9}, {0x24EB, 0x254B},
{0x2550, 0x2573}, {0x2580, 0x258F}, {0x2592, 0x2595},
{0x25A0, 0x25A1}, {0x25A3, 0x25A9}, {0x25B2, 0x25B3},
{0x25B6, 0x25B7}, {0x25BC, 0x25BD}, {0x25C0, 0x25C1},
{0x25C6, 0x25C8}, {0x25CB, 0x25CB}, {0x25CE, 0x25D1},
{0x25E2, 0x25E5}, {0x25EF, 0x25EF}, {0x2605, 0x2606},
{0x2609, 0x2609}, {0x260E, 0x260F}, {0x261C, 0x261C},
{0x261E, 0x261E}, {0x2640, 0x2640}, {0x2642, 0x2642},
{0x2660, 0x2661}, {0x2663, 0x2665}, {0x2667, 0x266A},
{0x266C, 0x266D}, {0x266F, 0x266F}, {0x269E, 0x269F},
{0x26BF, 0x26BF}, {0x26C6, 0x26CD}, {0x26CF, 0x26D3},
{0x26D5, 0x26E1}, {0x26E3, 0x26E3}, {0x26E8, 0x26E9},
{0x26EB, 0x26F1}, {0x26F4, 0x26F4}, {0x26F6, 0x26F9},
{0x26FB, 0x26FC}, {0x26FE, 0x26FF}, {0x273D, 0x273D},
{0x2776, 0x277F}, {0x2B56, 0x2B59}, {0x3248, 0x324F},
{0xE000, 0xF8FF}, {0xFFFD, 0xFFFD}, {0x1F100, 0x1F10A},
{0x1F110, 0x1F12D}, {0x1F130, 0x1F169}, {0x1F170, 0x1F18D},
{0x1F18F, 0x1F190}, {0x1F19B, 0x1F1AC}, {0xF0000, 0xFFFFD},
{0x100000, 0x10FFFD},
}
var ambiguousLatin = table{
{0x00AA, 0x00AA}, {0x00BA, 0x00BA}, {0x00C6, 0x00C6},
{0x00D0, 0x00D0}, {0x00D8, 0x00D8}, {0x00DE, 0x00E1},
{0x00E6, 0x00E6}, {0x00E8, 0x00EA}, {0x00EC, 0x00ED},
{0x00F0, 0x00F0}, {0x00F2, 0x00F3}, {0x00F7, 0x00FA},
{0x00F0, 0x00F0}, {0x00F2, 0x00F3}, {0x00F8, 0x00FA},
{0x00FC, 0x00FC}, {0x00FE, 0x00FE}, {0x0101, 0x0101},
{0x0111, 0x0111}, {0x0113, 0x0113}, {0x011B, 0x011B},
{0x0126, 0x0127}, {0x012B, 0x012B}, {0x0131, 0x0133},
{0x0138, 0x0138}, {0x013F, 0x0142}, {0x0144, 0x0144},
{0x0148, 0x014B}, {0x014D, 0x014D}, {0x0152, 0x0153},
{0x0166, 0x0167}, {0x016B, 0x016B}, {0x01CE, 0x01CE},
{0x01D0, 0x01D0}, {0x01D2, 0x01D2}, {0x01D4, 0x01D4},
{0x01D6, 0x01D6}, {0x01D8, 0x01D8}, {0x01DA, 0x01DA},
{0x01DC, 0x01DC}, {0x0251, 0x0251}, {0x0261, 0x0261},
{0x02C4, 0x02C4}, {0x02C7, 0x02C7}, {0x02C9, 0x02CB},
{0x02CD, 0x02CD}, {0x02D0, 0x02D0}, {0x02D8, 0x02DB},
{0x02DD, 0x02DD}, {0x02DF, 0x02DF}, {0x0300, 0x036F},
{0x0391, 0x03A1}, {0x03A3, 0x03A9}, {0x03B1, 0x03C1},
{0x03C3, 0x03C9}, {0x0401, 0x0401}, {0x0410, 0x044F},
{0x0451, 0x0451}, {0x2010, 0x2010}, {0x2013, 0x2016},
{0x2018, 0x2019}, {0x201C, 0x201D}, {0x2020, 0x2022},
{0x2024, 0x2027}, {0x2030, 0x2030}, {0x2032, 0x2033},
{0x2035, 0x2035}, {0x203B, 0x203B}, {0x203E, 0x203E},
{0x2074, 0x2074}, {0x207F, 0x207F}, {0x2081, 0x2084},
{0x20AC, 0x20AC}, {0x2103, 0x2103}, {0x2105, 0x2105},
{0x2109, 0x2109}, {0x2113, 0x2113}, {0x2116, 0x2116},
{0x2121, 0x2122}, {0x2126, 0x2126}, {0x212B, 0x212B},
{0x2153, 0x2154}, {0x215B, 0x215E}, {0x2160, 0x216B},
{0x2170, 0x2179}, {0x2189, 0x2189}, {0x2190, 0x2199},
{0x21B8, 0x21B9}, {0x21D2, 0x21D2}, {0x21D4, 0x21D4},
{0x21E7, 0x21E7}, {0x2200, 0x2200}, {0x2202, 0x2203},
{0x2207, 0x2208}, {0x220B, 0x220B}, {0x220F, 0x220F},
{0x2211, 0x2211}, {0x2215, 0x2215}, {0x221A, 0x221A},
{0x221D, 0x2220}, {0x2223, 0x2223}, {0x2225, 0x2225},
{0x2227, 0x222C}, {0x222E, 0x222E}, {0x2234, 0x2237},
{0x223C, 0x223D}, {0x2248, 0x2248}, {0x224C, 0x224C},
{0x2252, 0x2252}, {0x2260, 0x2261}, {0x2264, 0x2267},
{0x226A, 0x226B}, {0x226E, 0x226F}, {0x2282, 0x2283},
{0x2286, 0x2287}, {0x2295, 0x2295}, {0x2299, 0x2299},
{0x22A5, 0x22A5}, {0x22BF, 0x22BF}, {0x2312, 0x2312},
{0x2460, 0x24E9}, {0x24EB, 0x254B}, {0x2550, 0x2573},
{0x2580, 0x258F}, {0x2592, 0x2595}, {0x25A0, 0x25A1},
{0x25A3, 0x25A9}, {0x25B2, 0x25B3}, {0x25B6, 0x25B7},
{0x25BC, 0x25BD}, {0x25C0, 0x25C1}, {0x25C6, 0x25C8},
{0x25CB, 0x25CB}, {0x25CE, 0x25D1}, {0x25E2, 0x25E5},
{0x25EF, 0x25EF}, {0x2605, 0x2606}, {0x2609, 0x2609},
{0x260E, 0x260F}, {0x261C, 0x261C}, {0x261E, 0x261E},
{0x2640, 0x2640}, {0x2642, 0x2642}, {0x2660, 0x2661},
{0x2663, 0x2665}, {0x2667, 0x266A}, {0x266C, 0x266D},
{0x266F, 0x266F}, {0x269E, 0x269F}, {0x26BF, 0x26BF},
{0x26C6, 0x26CD}, {0x26CF, 0x26D3}, {0x26D5, 0x26E1},
{0x26E3, 0x26E3}, {0x26E8, 0x26E9}, {0x26EB, 0x26F1},
{0x26F4, 0x26F4}, {0x26F6, 0x26F9}, {0x26FB, 0x26FC},
{0x26FE, 0x26FF}, {0x273D, 0x273D}, {0x2776, 0x277F},
{0x2B56, 0x2B59}, {0x3248, 0x324F}, {0xE000, 0xF8FF},
{0xFFFD, 0xFFFD}, {0x1F100, 0x1F10A}, {0x1F110, 0x1F12D},
{0x1F130, 0x1F169}, {0x1F170, 0x1F18D}, {0x1F18F, 0x1F190},
{0x1F19B, 0x1F1AC}, {0xF0000, 0xFFFFD}, {0x100000, 0x10FFFD},
{0x0166, 0x0167}, {0x016B, 0x016B},
}
var narrow = table{
{0x0020, 0x007E}, {0x00A2, 0x00A3}, {0x00A5, 0x00A6},
{0x00AC, 0x00AC}, {0x00AF, 0x00AF}, {0x27E6, 0x27ED},
@@ -590,3 +596,120 @@ var emoji = table{
{0x1F947, 0x1F9FF}, {0x1FA58, 0x1FA5F}, {0x1FA6E, 0x1FAFF},
{0x1FC00, 0x1FFFD},
}
var joiner = table{
{0x0300, 0x036F}, {0x0483, 0x0489}, {0x0591, 0x05BD},
{0x05BF, 0x05BF}, {0x05C1, 0x05C2}, {0x05C4, 0x05C5},
{0x05C7, 0x05C7}, {0x0600, 0x0605}, {0x0610, 0x061A},
{0x064B, 0x065F}, {0x0670, 0x0670}, {0x06D6, 0x06DD},
{0x06DF, 0x06E4}, {0x06E7, 0x06E8}, {0x06EA, 0x06ED},
{0x070F, 0x070F}, {0x0711, 0x0711}, {0x0730, 0x074A},
{0x07A6, 0x07B0}, {0x07EB, 0x07F3}, {0x07FD, 0x07FD},
{0x0816, 0x0819}, {0x081B, 0x0823}, {0x0825, 0x0827},
{0x0829, 0x082D}, {0x0859, 0x085B}, {0x0890, 0x0891},
{0x0897, 0x089F}, {0x08CA, 0x0903}, {0x093A, 0x093C},
{0x093E, 0x094F}, {0x0951, 0x0957}, {0x0962, 0x0963},
{0x0981, 0x0983}, {0x09BC, 0x09BC}, {0x09BE, 0x09C4},
{0x09C7, 0x09C8}, {0x09CB, 0x09CD}, {0x09D7, 0x09D7},
{0x09E2, 0x09E3}, {0x09FE, 0x09FE}, {0x0A01, 0x0A03},
{0x0A3C, 0x0A3C}, {0x0A3E, 0x0A42}, {0x0A47, 0x0A48},
{0x0A4B, 0x0A4D}, {0x0A51, 0x0A51}, {0x0A70, 0x0A71},
{0x0A75, 0x0A75}, {0x0A81, 0x0A83}, {0x0ABC, 0x0ABC},
{0x0ABE, 0x0AC5}, {0x0AC7, 0x0AC9}, {0x0ACB, 0x0ACD},
{0x0AE2, 0x0AE3}, {0x0AFA, 0x0AFF}, {0x0B01, 0x0B03},
{0x0B3C, 0x0B3C}, {0x0B3E, 0x0B44}, {0x0B47, 0x0B48},
{0x0B4B, 0x0B4D}, {0x0B55, 0x0B57}, {0x0B62, 0x0B63},
{0x0B82, 0x0B82}, {0x0BBE, 0x0BC2}, {0x0BC6, 0x0BC8},
{0x0BCA, 0x0BCD}, {0x0BD7, 0x0BD7}, {0x0C00, 0x0C04},
{0x0C3C, 0x0C3C}, {0x0C3E, 0x0C44}, {0x0C46, 0x0C48},
{0x0C4A, 0x0C4D}, {0x0C55, 0x0C56}, {0x0C62, 0x0C63},
{0x0C81, 0x0C83}, {0x0CBC, 0x0CBC}, {0x0CBE, 0x0CC4},
{0x0CC6, 0x0CC8}, {0x0CCA, 0x0CCD}, {0x0CD5, 0x0CD6},
{0x0CE2, 0x0CE3}, {0x0CF3, 0x0CF3}, {0x0D00, 0x0D03},
{0x0D3B, 0x0D3C}, {0x0D3E, 0x0D44}, {0x0D46, 0x0D48},
{0x0D4A, 0x0D4E}, {0x0D57, 0x0D57}, {0x0D62, 0x0D63},
{0x0D81, 0x0D83}, {0x0DCA, 0x0DCA}, {0x0DCF, 0x0DD4},
{0x0DD6, 0x0DD6}, {0x0DD8, 0x0DDF}, {0x0DF2, 0x0DF3},
{0x0E31, 0x0E31}, {0x0E33, 0x0E3A}, {0x0E47, 0x0E4E},
{0x0EB1, 0x0EB1}, {0x0EB3, 0x0EBC}, {0x0EC8, 0x0ECE},
{0x0F18, 0x0F19}, {0x0F35, 0x0F35}, {0x0F37, 0x0F37},
{0x0F39, 0x0F39}, {0x0F3E, 0x0F3F}, {0x0F71, 0x0F84},
{0x0F86, 0x0F87}, {0x0F8D, 0x0F97}, {0x0F99, 0x0FBC},
{0x0FC6, 0x0FC6}, {0x102D, 0x1037}, {0x1039, 0x103E},
{0x1056, 0x1059}, {0x105E, 0x1060}, {0x1071, 0x1074},
{0x1082, 0x1082}, {0x1084, 0x1086}, {0x108D, 0x108D},
{0x109D, 0x109D}, {0x1100, 0x11FF}, {0x135D, 0x135F},
{0x1712, 0x1715}, {0x1732, 0x1734}, {0x1752, 0x1753},
{0x1772, 0x1773}, {0x17B4, 0x17D3}, {0x17DD, 0x17DD},
{0x180B, 0x180D}, {0x180F, 0x180F}, {0x1885, 0x1886},
{0x18A9, 0x18A9}, {0x1920, 0x192B}, {0x1930, 0x193B},
{0x1A17, 0x1A1B}, {0x1A55, 0x1A5E}, {0x1A60, 0x1A60},
{0x1A62, 0x1A62}, {0x1A65, 0x1A7C}, {0x1A7F, 0x1A7F},
{0x1AB0, 0x1ADD}, {0x1AE0, 0x1AEB}, {0x1B00, 0x1B04},
{0x1B34, 0x1B44}, {0x1B6B, 0x1B73}, {0x1B80, 0x1B82},
{0x1BA1, 0x1BAD}, {0x1BE6, 0x1BF3}, {0x1C24, 0x1C37},
{0x1CD0, 0x1CD2}, {0x1CD4, 0x1CE8}, {0x1CED, 0x1CED},
{0x1CF4, 0x1CF4}, {0x1CF7, 0x1CF9}, {0x1DC0, 0x1DFF},
{0x200C, 0x200D}, {0x20D0, 0x20F0}, {0x2CEF, 0x2CF1},
{0x2D7F, 0x2D7F}, {0x2DE0, 0x2DFF}, {0x302A, 0x302F},
{0x3099, 0x309A}, {0xA66F, 0xA672}, {0xA674, 0xA67D},
{0xA69E, 0xA69F}, {0xA6F0, 0xA6F1}, {0xA802, 0xA802},
{0xA806, 0xA806}, {0xA80B, 0xA80B}, {0xA823, 0xA827},
{0xA82C, 0xA82C}, {0xA880, 0xA881}, {0xA8B4, 0xA8C5},
{0xA8E0, 0xA8F1}, {0xA8FF, 0xA8FF}, {0xA926, 0xA92D},
{0xA947, 0xA953}, {0xA960, 0xA97C}, {0xA980, 0xA983},
{0xA9B3, 0xA9C0}, {0xA9E5, 0xA9E5}, {0xAA29, 0xAA36},
{0xAA43, 0xAA43}, {0xAA4C, 0xAA4D}, {0xAA7C, 0xAA7C},
{0xAAB0, 0xAAB0}, {0xAAB2, 0xAAB4}, {0xAAB7, 0xAAB8},
{0xAABE, 0xAABF}, {0xAAC1, 0xAAC1}, {0xAAEB, 0xAAEF},
{0xAAF5, 0xAAF6}, {0xABE3, 0xABEA}, {0xABEC, 0xABED},
{0xAC00, 0xD7A3}, {0xD7B0, 0xD7C6}, {0xD7CB, 0xD7FB},
{0xFB1E, 0xFB1E}, {0xFE00, 0xFE0F}, {0xFE20, 0xFE2F},
{0xFF9E, 0xFF9F}, {0x101FD, 0x101FD}, {0x102E0, 0x102E0},
{0x10376, 0x1037A}, {0x10A01, 0x10A03}, {0x10A05, 0x10A06},
{0x10A0C, 0x10A0F}, {0x10A38, 0x10A3A}, {0x10A3F, 0x10A3F},
{0x10AE5, 0x10AE6}, {0x10D24, 0x10D27}, {0x10D69, 0x10D6D},
{0x10EAB, 0x10EAC}, {0x10EFA, 0x10EFF}, {0x10F46, 0x10F50},
{0x10F82, 0x10F85}, {0x11000, 0x11002}, {0x11038, 0x11046},
{0x11070, 0x11070}, {0x11073, 0x11074}, {0x1107F, 0x11082},
{0x110B0, 0x110BA}, {0x110BD, 0x110BD}, {0x110C2, 0x110C2},
{0x110CD, 0x110CD}, {0x11100, 0x11102}, {0x11127, 0x11134},
{0x11145, 0x11146}, {0x11173, 0x11173}, {0x11180, 0x11182},
{0x111B3, 0x111C0}, {0x111C2, 0x111C3}, {0x111C9, 0x111CC},
{0x111CE, 0x111CF}, {0x1122C, 0x11237}, {0x1123E, 0x1123E},
{0x11241, 0x11241}, {0x112DF, 0x112EA}, {0x11300, 0x11303},
{0x1133B, 0x1133C}, {0x1133E, 0x11344}, {0x11347, 0x11348},
{0x1134B, 0x1134D}, {0x11357, 0x11357}, {0x11362, 0x11363},
{0x11366, 0x1136C}, {0x11370, 0x11374}, {0x113B8, 0x113C0},
{0x113C2, 0x113C2}, {0x113C5, 0x113C5}, {0x113C7, 0x113CA},
{0x113CC, 0x113D2}, {0x113E1, 0x113E2}, {0x11435, 0x11446},
{0x1145E, 0x1145E}, {0x114B0, 0x114C3}, {0x115AF, 0x115B5},
{0x115B8, 0x115C0}, {0x115DC, 0x115DD}, {0x11630, 0x11640},
{0x116AB, 0x116B7}, {0x1171D, 0x1171F}, {0x11722, 0x1172B},
{0x1182C, 0x1183A}, {0x11930, 0x11935}, {0x11937, 0x11938},
{0x1193B, 0x11943}, {0x119D1, 0x119D7}, {0x119DA, 0x119E0},
{0x119E4, 0x119E4}, {0x11A01, 0x11A0A}, {0x11A33, 0x11A3E},
{0x11A47, 0x11A47}, {0x11A51, 0x11A5B}, {0x11A84, 0x11A99},
{0x11B60, 0x11B67}, {0x11C2F, 0x11C36}, {0x11C38, 0x11C3F},
{0x11C92, 0x11CA7}, {0x11CA9, 0x11CB6}, {0x11D31, 0x11D36},
{0x11D3A, 0x11D3A}, {0x11D3C, 0x11D3D}, {0x11D3F, 0x11D47},
{0x11D8A, 0x11D8E}, {0x11D90, 0x11D91}, {0x11D93, 0x11D97},
{0x11EF3, 0x11EF6}, {0x11F00, 0x11F03}, {0x11F34, 0x11F3A},
{0x11F3E, 0x11F42}, {0x11F5A, 0x11F5A}, {0x13440, 0x13440},
{0x13447, 0x13455}, {0x1611E, 0x1612F}, {0x16AF0, 0x16AF4},
{0x16B30, 0x16B36}, {0x16D63, 0x16D63}, {0x16D67, 0x16D6A},
{0x16F4F, 0x16F4F}, {0x16F51, 0x16F87}, {0x16F8F, 0x16F92},
{0x16FE4, 0x16FE4}, {0x16FF0, 0x16FF1}, {0x1BC9D, 0x1BC9E},
{0x1CF00, 0x1CF2D}, {0x1CF30, 0x1CF46}, {0x1D165, 0x1D169},
{0x1D16D, 0x1D172}, {0x1D17B, 0x1D182}, {0x1D185, 0x1D18B},
{0x1D1AA, 0x1D1AD}, {0x1D242, 0x1D244}, {0x1DA00, 0x1DA36},
{0x1DA3B, 0x1DA6C}, {0x1DA75, 0x1DA75}, {0x1DA84, 0x1DA84},
{0x1DA9B, 0x1DA9F}, {0x1DAA1, 0x1DAAF}, {0x1E000, 0x1E006},
{0x1E008, 0x1E018}, {0x1E01B, 0x1E021}, {0x1E023, 0x1E024},
{0x1E026, 0x1E02A}, {0x1E08F, 0x1E08F}, {0x1E130, 0x1E136},
{0x1E2AE, 0x1E2AE}, {0x1E2EC, 0x1E2EF}, {0x1E4EC, 0x1E4EF},
{0x1E5EE, 0x1E5EF}, {0x1E6E3, 0x1E6E3}, {0x1E6E6, 0x1E6E6},
{0x1E6EE, 0x1E6EF}, {0x1E6F5, 0x1E6F5}, {0x1E8D0, 0x1E8D6},
{0x1E944, 0x1E94A}, {0x1F1E6, 0x1F1FF}, {0x1F3FB, 0x1F3FF},
{0xE0020, 0xE007F}, {0xE0100, 0xE01EF},
}
+1 -1
View File
@@ -21,7 +21,7 @@ func Encode(authConfig registry.AuthConfig) (string, error) {
//
// FIXME(thaJeztah): find exactly what code-paths are impacted by this.
// if authConfig == (AuthConfig{}) { return "", nil }
buf, err := json.Marshal(authConfig)
buf, err := json.Marshal(authConfig) // #nosec G117 -- intentionally marshaling auth.
if err != nil {
return "", errInvalidParameter{err}
}
-3
View File
@@ -2,9 +2,6 @@
package build
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
// DiskUsage represents system data usage for build cache resources.
//
// swagger:model DiskUsage
-3
View File
@@ -2,9 +2,6 @@
package common
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
// ErrorResponse Represents an error.
// Example: {"message":"Something went wrong."}
//
-3
View File
@@ -2,9 +2,6 @@
package common
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
// IDResponse Response to an API call that returns just an Id
//
// swagger:model IDResponse
-3
View File
@@ -2,9 +2,6 @@
package container
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
// ChangeType Kind of change
//
// Can be one of:
-3
View File
@@ -2,9 +2,6 @@
package container
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
// CreateResponse ContainerCreateResponse
//
// # OK response to ContainerCreate operation
-3
View File
@@ -2,9 +2,6 @@
package container
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
// DiskUsage represents system data usage information for container resources.
//
// swagger:model DiskUsage
-3
View File
@@ -2,9 +2,6 @@
package container
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
// FilesystemChange Change in the container's filesystem.
//
// swagger:model FilesystemChange
-3
View File
@@ -2,9 +2,6 @@
package container
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/netip"
)
-3
View File
@@ -2,9 +2,6 @@
package container
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
// TopResponse ContainerTopResponse
//
// Container "top" response.
-3
View File
@@ -2,9 +2,6 @@
package container
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
// UpdateResponse ContainerUpdateResponse
//
// Response for a successful container-update.
-3
View File
@@ -2,9 +2,6 @@
package container
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
// WaitExitError container waiting error, if any
//
// swagger:model WaitExitError
-3
View File
@@ -2,9 +2,6 @@
package container
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
// WaitResponse ContainerWaitResponse
//
// # OK response to ContainerWait operation
-3
View File
@@ -2,9 +2,6 @@
package image
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
// DeleteResponse delete response
//
// swagger:model DeleteResponse
-3
View File
@@ -2,9 +2,6 @@
package image
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
// DiskUsage represents system data usage for image resources.
//
// swagger:model DiskUsage
-3
View File
@@ -2,9 +2,6 @@
package image
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
// HistoryResponseItem HistoryResponseItem
//
// individual image layer information in response to ImageHistory operation
-3
View File
@@ -2,9 +2,6 @@
package network
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
// ConfigReference The config-only network source to provide the configuration for
// this network.
//
-3
View File
@@ -2,9 +2,6 @@
package network
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
// ConnectRequest NetworkConnectRequest represents the data to be used to connect a container to a network.
//
// swagger:model ConnectRequest

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