mirror of
https://github.com/docker/cli.git
synced 2026-11-12 15:53:52 -04:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7c0f5c53c | ||
|
|
87fb2ebe7e | ||
|
|
e8c098c8bf | ||
|
|
374636ff15 | ||
|
|
862e7e6ad4 | ||
|
|
3d2650ce76 | ||
|
|
e11705385d | ||
|
|
fd58705f3a | ||
|
|
5e6d8e3ab6 | ||
|
|
8503a98b68 | ||
|
|
488292ca1f | ||
|
|
75d1d1a9e6 | ||
|
|
cb192aeb41 | ||
|
|
6cb63efe6f | ||
|
|
e8bb7f7c81 | ||
|
|
3ee4c06af6 |
@@ -68,14 +68,14 @@ jobs:
|
||||
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"
|
||||
|
||||
@@ -37,6 +37,7 @@ jobs:
|
||||
- alpine
|
||||
- debian
|
||||
engine-version:
|
||||
- master # moby/moby-bin:master
|
||||
- rc # latest rc
|
||||
- 29 # latest
|
||||
- 28 # latest - 1
|
||||
|
||||
+80
-14
@@ -7,9 +7,11 @@ import (
|
||||
"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"
|
||||
@@ -43,8 +45,11 @@ import (
|
||||
// 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, receives no interactive stdin, and sends progress
|
||||
// to stderr.
|
||||
// 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"}
|
||||
@@ -95,22 +100,31 @@ func processCloud(ctx context.Context, dockerCli *command.DockerCli, cmd *cobra.
|
||||
// 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 {
|
||||
return nil, fmt.Errorf("cloud resolver plugin %q unavailable: %w", provider, err)
|
||||
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 cloud resolver plugin %q: %w", provider, plugin.Err)
|
||||
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 context resolution", provider)
|
||||
return nil, fmt.Errorf("plugin %q does not support --cloud%s", provider, help)
|
||||
}
|
||||
|
||||
return plugin, nil
|
||||
@@ -139,7 +153,7 @@ func resolveCloudContext(ctx context.Context, dockerCli *command.DockerCli, root
|
||||
|
||||
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])
|
||||
cmd.Stderr = dockerCli.Err()
|
||||
setResolverStdio(cmd, dockerCli)
|
||||
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
@@ -160,21 +174,57 @@ func resolveCloudContext(ctx context.Context, dockerCli *command.DockerCli, root
|
||||
return "", errors.New("cloud resolver must return a non-default DOCKER_CONTEXT")
|
||||
}
|
||||
|
||||
// Do not allow a missing context or endpoint to fall back to the local engine.
|
||||
meta, err := dockerCli.ContextStore().GetMetadata(response.DockerContext)
|
||||
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", response.DockerContext, err)
|
||||
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", response.DockerContext, err)
|
||||
return fmt.Errorf("invalid resolved context %q: %w", name, err)
|
||||
}
|
||||
if endpoint.Host == "" {
|
||||
return "", fmt.Errorf("resolved context %q has no Docker endpoint host", response.DockerContext)
|
||||
return fmt.Errorf("resolved context %q has no Docker endpoint host", name)
|
||||
}
|
||||
|
||||
return response.DockerContext, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// cloudHelpRequest avoids provisioning for help and shell completion.
|
||||
@@ -199,7 +249,17 @@ func cloudHelpRequest(rootCmd *cobra.Command, args []string) (bool, error) {
|
||||
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 && (args[1] == "--help" || args[1] == "-h" || args[1] == "--help=true"), nil
|
||||
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()
|
||||
@@ -226,6 +286,12 @@ func cloudHelpRequest(rootCmd *cobra.Command, args []string) (bool, error) {
|
||||
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) {
|
||||
|
||||
+169
-29
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"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"
|
||||
@@ -62,6 +64,11 @@ func TestCloudHelpRequest(t *testing.T) {
|
||||
{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()
|
||||
@@ -169,19 +176,21 @@ func TestCloudResolution(t *testing.T) {
|
||||
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
|
||||
command []string
|
||||
visible bool
|
||||
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",
|
||||
@@ -276,28 +285,45 @@ func TestCloudResolution(t *testing.T) {
|
||||
args: []string{"--cloud"},
|
||||
noProvider: true,
|
||||
legacyProvider: true,
|
||||
wantErr: `plugin "offload" does not support cloud context resolution`,
|
||||
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 context resolution`,
|
||||
wantErr: `plugin "foobar" does not support --cloud`,
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "configured provider unavailable",
|
||||
args: []string{"--cloud"},
|
||||
provider: "missing",
|
||||
wantErr: `cloud resolver plugin "missing" unavailable`,
|
||||
wantErr: `plugin "missing" unavailable`,
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "provider cannot be a path",
|
||||
args: []string{"--cloud"},
|
||||
provider: "../foobar",
|
||||
wantErr: `cloud resolver plugin "../foobar" unavailable`,
|
||||
wantErr: `plugin "../foobar" unavailable`,
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
@@ -370,10 +396,7 @@ func TestCloudResolution(t *testing.T) {
|
||||
if !tc.noProvider {
|
||||
installedProvider = "foobar"
|
||||
cfg := configfile.New(filepath.Join(configDir, config.ConfigFileName))
|
||||
provider := tc.provider
|
||||
if provider == "" {
|
||||
provider = "foobar"
|
||||
}
|
||||
provider := cmp.Or(tc.provider, "foobar")
|
||||
cfg.Features = map[string]string{"cloud": provider}
|
||||
assert.NilError(t, cfg.Save())
|
||||
}
|
||||
@@ -381,12 +404,12 @@ func TestCloudResolution(t *testing.T) {
|
||||
pluginDir := filepath.Join(configDir, "cli-plugins")
|
||||
assert.NilError(t, os.MkdirAll(pluginDir, 0o755))
|
||||
|
||||
response := tc.response
|
||||
if response == "" {
|
||||
response = `{"DOCKER_CONTEXT":"resolved"}`
|
||||
}
|
||||
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}
|
||||
}
|
||||
@@ -448,6 +471,7 @@ printf '%s\n' "$@" > "$CLOUD_TEST_ARGS"
|
||||
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(), "")
|
||||
@@ -473,16 +497,132 @@ printf '%s\n' "$@" > "$CLOUD_TEST_ARGS"
|
||||
} else {
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Contains(stderr.String(), "provisioning"))
|
||||
name := tc.wantName
|
||||
if name == "" {
|
||||
name = "default"
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -31,6 +31,7 @@ services:
|
||||
dockerfile: engine/Dockerfile
|
||||
args:
|
||||
ENGINE_VERSION: ${ENGINE_VERSION:-29}
|
||||
ENGINE_SOURCE: ${ENGINE_SOURCE:-dind}
|
||||
privileged: true
|
||||
command: ['--insecure-registry=registry:5000', '--insecure-registry=privateregistry:5001', '--experimental']
|
||||
environment:
|
||||
|
||||
@@ -112,7 +112,7 @@ func TestRunWithCgroupNamespace(t *testing.T) {
|
||||
|
||||
func TestRunUmask(t *testing.T) {
|
||||
environment.SkipIfDaemonNotLinux(t)
|
||||
t.Skip("FIXME: un-skip once e2e tests v29.8.0")
|
||||
skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.56"))
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
+9
-1
@@ -3,8 +3,16 @@
|
||||
# ENGINE_VERSION is the version of the (docker-in-docker) Docker Engine to
|
||||
# test against.
|
||||
ARG ENGINE_VERSION=29
|
||||
# ENGINE_SOURCE is "dind" or "moby-bin" (moby/moby-bin:master).
|
||||
ARG ENGINE_SOURCE=dind
|
||||
|
||||
FROM docker:${ENGINE_VERSION}-dind
|
||||
FROM docker:${ENGINE_VERSION}-dind AS engine-dind
|
||||
|
||||
# moby/moby-bin only has the daemon binaries.
|
||||
FROM docker:dind AS engine-moby-bin
|
||||
COPY --link --from=moby/moby-bin:master / /usr/local/bin/
|
||||
|
||||
FROM engine-${ENGINE_SOURCE}
|
||||
|
||||
# the openssh-client update is needed for security reasons when using docker:23.0-dind, currently maintained as an lts by mirantis
|
||||
RUN apk --no-cache add openssl openssh-client openssh-server shadow && \
|
||||
|
||||
Vendored
+10
-1
@@ -1,5 +1,14 @@
|
||||
ARG ENGINE_VERSION
|
||||
FROM docker:${ENGINE_VERSION}-dind
|
||||
# ENGINE_SOURCE is "dind" or "moby-bin" (moby/moby-bin:master).
|
||||
ARG ENGINE_SOURCE=dind
|
||||
|
||||
FROM docker:${ENGINE_VERSION}-dind AS engine-dind
|
||||
|
||||
# moby/moby-bin only has the daemon binaries.
|
||||
FROM docker:dind AS engine-moby-bin
|
||||
COPY --link --from=moby/moby-bin:master / /usr/local/bin/
|
||||
|
||||
FROM engine-${ENGINE_SOURCE}
|
||||
|
||||
# Trust the tlsregistry CA so dockerd connects without --insecure-registry.
|
||||
COPY registry/certs/ca.crt /usr/local/share/ca-certificates/tlsregistry-ca.crt
|
||||
|
||||
@@ -42,6 +42,11 @@ setup() {
|
||||
sh e2e/testdata/registry/certs/gen-certs.sh
|
||||
fi
|
||||
|
||||
# ENGINE_VERSION=master tests against moby/moby-bin:master.
|
||||
if [ "${ENGINE_VERSION:-}" = "master" ]; then
|
||||
export ENGINE_SOURCE=moby-bin
|
||||
fi
|
||||
|
||||
COMPOSE_PROJECT_NAME=$project COMPOSE_FILE=$file docker compose up --build -d >&2
|
||||
|
||||
# Ensure supporting services exist before running tests. If one fails to start,
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ 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.30
|
||||
github.com/moby/go-archive v0.3.3
|
||||
github.com/moby/moby/api v1.56.1
|
||||
github.com/moby/moby/client v0.6.1
|
||||
|
||||
+2
-2
@@ -82,8 +82,8 @@ github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLM
|
||||
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.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk=
|
||||
github.com/mattn/go-runewidth v0.0.30/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=
|
||||
|
||||
+244
-8
@@ -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()
|
||||
}
|
||||
@@ -446,6 +460,60 @@ 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -489,6 +557,13 @@ 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() {
|
||||
@@ -497,12 +572,29 @@ graphemes:
|
||||
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)
|
||||
@@ -517,12 +609,45 @@ 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)
|
||||
|
||||
@@ -547,6 +672,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,6 +709,9 @@ 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)
|
||||
@@ -573,29 +727,111 @@ 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
|
||||
}
|
||||
// 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(s)
|
||||
for g.Next() {
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
+117
@@ -590,3 +590,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},
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -155,7 +155,7 @@ github.com/klauspost/compress/internal/le
|
||||
github.com/klauspost/compress/internal/snapref
|
||||
github.com/klauspost/compress/zstd
|
||||
github.com/klauspost/compress/zstd/internal/xxhash
|
||||
# github.com/mattn/go-runewidth v0.0.29
|
||||
# github.com/mattn/go-runewidth v0.0.30
|
||||
## explicit; go 1.23
|
||||
github.com/mattn/go-runewidth
|
||||
# github.com/moby/docker-image-spec v1.3.1
|
||||
|
||||
Reference in New Issue
Block a user