Merge pull request #3883 from crazy-max/local-output-delete

support BuildKit local output delete mode
This commit is contained in:
Tõnis Tiigi
2026-06-10 14:14:12 -07:00
committed by GitHub
262 changed files with 30312 additions and 2455 deletions
+28 -121
View File
@@ -6,17 +6,17 @@ import (
"context"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"syscall"
"github.com/containerd/console"
"github.com/docker/buildx/build"
"github.com/docker/buildx/util/buildflags"
"github.com/docker/buildx/util/osutil"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/util/entitlements"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@@ -36,6 +36,7 @@ const (
EntitlementKeyImageLoad EntitlementKey = "image.load"
EntitlementKeyImage EntitlementKey = "image"
EntitlementKeySSH EntitlementKey = "ssh"
EntitlementKeyBuildxLocalDelete EntitlementKey = EntitlementKey(buildflags.EntitlementBuildxLocalDelete)
)
type EntitlementConf struct {
@@ -47,6 +48,7 @@ type EntitlementConf struct {
ImagePush []string
ImageLoad []string
SSH bool
LocalOutputDelete bool
}
type EntitlementsDevicesConf struct {
@@ -64,6 +66,8 @@ func ParseEntitlements(in []string) (EntitlementConf, error) {
conf.SecurityInsecure = true
case string(EntitlementKeySSH):
conf.SSH = true
case string(EntitlementKeyBuildxLocalDelete):
conf.LocalOutputDelete = true
default:
k, v, _ := strings.Cut(e, "=")
switch k {
@@ -97,6 +101,8 @@ func ParseEntitlements(in []string) (EntitlementConf, error) {
case string(EntitlementKeyImage):
conf.ImagePush = append(conf.ImagePush, v)
conf.ImageLoad = append(conf.ImageLoad, v)
case string(EntitlementKeyBuildxLocalDelete):
return conf, errors.Errorf("%s does not accept a value", EntitlementKeyBuildxLocalDelete)
default:
return conf, errors.Errorf("unknown entitlement key %q", k)
}
@@ -164,6 +170,19 @@ func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) erro
rwPaths[p] = struct{}{}
}
for _, ex := range bo.Exports {
if ex.Type != client.ExporterLocal {
continue
}
mode, err := client.ParseLocalExporterMode(ex.Attrs["mode"])
if err != nil {
return err
}
if mode == client.LocalExporterModeDelete && !c.LocalOutputDelete {
expected.LocalOutputDelete = true
}
}
for _, ce := range bo.CacheTo {
if ce.Type == "local" {
if dest, ok := ce.Attrs["dest"]; ok {
@@ -249,6 +268,10 @@ func (c EntitlementConf) Prompt(ctx context.Context, isRemote bool, out io.Write
msgsFS = append(msgsFS, " - Forwarding default SSH agent socket")
flagsFS = append(flagsFS, string(EntitlementKeySSH))
}
if c.LocalOutputDelete {
msgs = append(msgs, " - Deleting stale files from local output destinations")
flags = append(flags, string(EntitlementKeyBuildxLocalDelete))
}
roPaths, rwPaths, commonPaths := groupSamePaths(c.FSRead, c.FSWrite)
wd, err := os.Getwd()
@@ -506,7 +529,7 @@ func evaluatePaths(in []string) ([]string, bool, error) {
logrus.Warnf("failed to evaluate entitlement path %q: %v", p, err)
continue
}
v, rest, err := evaluateToExistingPath(v)
v, rest, err := osutil.EvaluateToExistingPath(v)
if err != nil {
return nil, false, errors.Wrapf(err, "failed to evaluate path %q", p)
}
@@ -525,7 +548,7 @@ func evaluatePaths(in []string) ([]string, bool, error) {
func evaluateToExistingPaths(in map[string]struct{}) (map[string]struct{}, error) {
m := make(map[string]struct{}, len(in))
for p := range in {
v, _, err := evaluateToExistingPath(p)
v, _, err := osutil.EvaluateToExistingPath(p)
if err != nil {
return nil, errors.Wrapf(err, "failed to evaluate path %q", p)
}
@@ -539,121 +562,5 @@ func evaluateToExistingPaths(in map[string]struct{}) (map[string]struct{}, error
}
func evaluateToExistingPath(in string) (string, string, error) {
in, err := filepath.Abs(in)
if err != nil {
return "", "", err
}
volLen := volumeNameLen(in)
pathSeparator := string(os.PathSeparator)
if volLen < len(in) && os.IsPathSeparator(in[volLen]) {
volLen++
}
vol := in[:volLen]
dest := vol
linksWalked := 0
var end int
for start := volLen; start < len(in); start = end {
for start < len(in) && os.IsPathSeparator(in[start]) {
start++
}
end = start
for end < len(in) && !os.IsPathSeparator(in[end]) {
end++
}
if end == start {
break
} else if in[start:end] == "." {
continue
} else if in[start:end] == ".." {
var r int
for r = len(dest) - 1; r >= volLen; r-- {
if os.IsPathSeparator(dest[r]) {
break
}
}
if r < volLen || dest[r+1:] == ".." {
if len(dest) > volLen {
dest += pathSeparator
}
dest += ".."
} else {
dest = dest[:r]
}
continue
}
if len(dest) > volumeNameLen(dest) && !os.IsPathSeparator(dest[len(dest)-1]) {
dest += pathSeparator
}
dest += in[start:end]
fi, err := os.Lstat(dest)
if err != nil {
// If the component doesn't exist, return the last valid path
if os.IsNotExist(err) {
for r := len(dest) - 1; r >= volLen; r-- {
if os.IsPathSeparator(dest[r]) {
return dest[:r], in[start:], nil
}
}
return vol, in[start:], nil
}
return "", "", err
}
if fi.Mode()&fs.ModeSymlink == 0 {
if !fi.Mode().IsDir() && end < len(in) {
return "", "", syscall.ENOTDIR
}
continue
}
linksWalked++
if linksWalked > 255 {
return "", "", errors.New("too many symlinks")
}
link, err := os.Readlink(dest)
if err != nil {
return "", "", err
}
in = link + in[end:]
v := volumeNameLen(link)
if v > 0 {
if v < len(link) && os.IsPathSeparator(link[v]) {
v++
}
vol = link[:v]
dest = vol
end = len(vol)
} else if len(link) > 0 && os.IsPathSeparator(link[0]) {
dest = link[:1]
end = 1
vol = link[:1]
volLen = 1
} else {
var r int
for r = len(dest) - 1; r >= volLen; r-- {
if os.IsPathSeparator(dest[r]) {
break
}
}
if r < volLen {
dest = vol
} else {
dest = dest[:r]
}
end = 0
}
}
return filepath.Clean(dest), "", nil
}
func volumeNameLen(s string) int {
return len(filepath.VolumeName(s))
return osutil.EvaluateToExistingPath(in)
}
+111
View File
@@ -1,6 +1,8 @@
package bake
import (
"bytes"
"context"
"fmt"
"os"
"path/filepath"
@@ -10,6 +12,7 @@ import (
"github.com/docker/buildx/build"
"github.com/docker/buildx/util/buildflags"
"github.com/docker/buildx/util/osutil"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/util/entitlements"
"github.com/stretchr/testify/require"
@@ -405,6 +408,73 @@ func TestValidateEntitlements(t *testing.T) {
FSWrite: []string{dir1},
},
},
{
name: "LocalOutputDeleteMissing",
conf: EntitlementConf{
FSWrite: []string{"*"},
},
opt: build.Options{
Inputs: build.Inputs{
ContextState: &llb.State{},
},
Exports: []client.ExportEntry{
{
Type: client.ExporterLocal,
OutputDir: dir1,
Attrs: map[string]string{
"mode": string(client.LocalExporterModeDelete),
},
},
},
ExportsLocalPathsTemporary: []string{dir1},
},
expected: EntitlementConf{
LocalOutputDelete: true,
},
},
{
name: "LocalOutputDeleteSet",
conf: EntitlementConf{
FSWrite: []string{"*"},
LocalOutputDelete: true,
},
opt: build.Options{
Inputs: build.Inputs{
ContextState: &llb.State{},
},
Exports: []client.ExportEntry{
{
Type: client.ExporterLocal,
OutputDir: dir1,
Attrs: map[string]string{
"mode": string(client.LocalExporterModeDelete),
},
},
},
ExportsLocalPathsTemporary: []string{dir1},
},
},
{
name: "LocalOutputCopy",
conf: EntitlementConf{
FSWrite: []string{"*"},
},
opt: build.Options{
Inputs: build.Inputs{
ContextState: &llb.State{},
},
Exports: []client.ExportEntry{
{
Type: client.ExporterLocal,
OutputDir: dir1,
Attrs: map[string]string{
"mode": string(client.LocalExporterModeCopy),
},
},
},
ExportsLocalPathsTemporary: []string{dir1},
},
},
}
for _, tc := range tcases {
@@ -416,6 +486,47 @@ func TestValidateEntitlements(t *testing.T) {
}
}
func TestValidateEntitlementsInvalidLocalOutputMode(t *testing.T) {
_, err := EntitlementConf{}.Validate(map[string]build.Options{
"test": {
Inputs: build.Inputs{
ContextState: &llb.State{},
},
Exports: []client.ExportEntry{
{
Type: client.ExporterLocal,
Attrs: map[string]string{
"mode": "backup",
},
},
},
},
})
require.ErrorContains(t, err, `invalid local exporter mode "backup"`)
}
func TestParseEntitlementsLocalOutputDelete(t *testing.T) {
conf, err := ParseEntitlements([]string{string(EntitlementKeyBuildxLocalDelete)})
require.NoError(t, err)
require.True(t, conf.LocalOutputDelete)
_, err = ParseEntitlements([]string{string(EntitlementKeyBuildxLocalDelete) + "=true"})
require.ErrorContains(t, err, "buildx.local.delete does not accept a value")
}
func TestPromptLocalOutputDeleteCannotBeDisabledWithFSEntitlements(t *testing.T) {
t.Setenv("BUILDX_BAKE_ENTITLEMENTS_FS", "0")
ctx, cancel := context.WithCancelCause(context.Background())
cancel(nil)
var out bytes.Buffer
err := EntitlementConf{LocalOutputDelete: true}.Prompt(ctx, true, &out)
require.ErrorContains(t, err, "additional privileges requested")
require.Contains(t, out.String(), "Deleting stale files from local output destinations")
require.Contains(t, out.String(), "--allow=buildx.local.delete")
}
func TestGroupSamePaths(t *testing.T) {
tests := []struct {
name string
+63
View File
@@ -1355,6 +1355,69 @@ func CreateExports(entries []*buildflags.ExportEntry) ([]client.ExportEntry, []s
return outs, localPaths, nil
}
func ValidateLocalExportDelete(outputs []client.ExportEntry, allowDelete bool) error {
for _, ex := range outputs {
if ex.Type != client.ExporterLocal {
continue
}
mode, err := client.ParseLocalExporterMode(ex.Attrs["mode"])
if err != nil {
return err
}
if mode != client.LocalExporterModeDelete || allowDelete {
continue
}
ok, err := isSafeLocalDeleteDest(ex.OutputDir)
if err != nil {
return err
}
if !ok {
return errors.Errorf("local output mode=delete for destination %q requires --allow=%s", ex.OutputDir, buildflags.EntitlementBuildxLocalDelete)
}
}
return nil
}
func isSafeLocalDeleteDest(dest string) (bool, error) {
wd, err := os.Getwd()
if err != nil {
return false, errors.Wrap(err, "failed to get current working directory")
}
wd, err = resolveOutputPath(wd)
if err != nil {
return false, errors.Wrap(err, "failed to evaluate current working directory")
}
dest, err = resolveOutputPath(dest)
if err != nil {
return false, errors.Wrapf(err, "failed to evaluate local output destination %q", dest)
}
rel, err := filepath.Rel(wd, dest)
if err != nil {
return false, nil
}
if rel == "." || rel == ".." || filepath.IsAbs(rel) || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return false, nil
}
return true, nil
}
func resolveOutputPath(p string) (string, error) {
p, rest, err := osutil.EvaluateToExistingPath(p)
if err != nil {
return "", err
}
p, err = osutil.GetLongPathName(p)
if err != nil {
return "", err
}
if rest != "" {
p = filepath.Join(p, rest)
}
return filepath.Clean(p), nil
}
func wrapWriteCloser(wc io.WriteCloser) func(map[string]string) (io.WriteCloser, error) {
return func(map[string]string) (io.WriteCloser, error) {
return wc, nil
+5 -1
View File
@@ -329,7 +329,11 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba
if err != nil {
return err
}
if progressMode != progressui.RawJSONMode {
if progressMode == progressui.RawJSONMode {
if exp.LocalOutputDelete {
return errors.Errorf("additional privileges requested: pass %q to grant requested privileges", "--allow="+string(bake.EntitlementKeyBuildxLocalDelete))
}
} else {
if err := exp.Prompt(ctx, url != "", &syncWriter{w: dockerCli.Err(), wait: printer.Wait}); err != nil {
return err
}
+9 -5
View File
@@ -554,7 +554,7 @@ func buildCmd(dockerCli command.Cli, rootOpts *rootOptions, debugger debuggerOpt
flags.StringSliceVar(&options.extraHosts, "add-host", []string{}, `Add a custom host-to-IP mapping (format: "host:ip")`)
flags.StringArrayVar(&options.allow, "allow", []string{}, `Allow extra privileged entitlement (e.g., "network.host", "security.insecure", "device")`)
flags.StringArrayVar(&options.allow, "allow", []string{}, `Allow extra privileged entitlement (e.g., "network.host", "security.insecure", "device", "buildx.local.delete")`)
flags.StringArrayVarP(&options.annotations, "annotation", "", []string{}, "Add annotation to the image")
@@ -1138,6 +1138,14 @@ func RunBuild(ctx context.Context, dockerCli command.Cli, in *BuildOptions, inSt
}
}
allow, allowLocalOutputDelete, err := buildflags.ParseEntitlements(in.Allow)
if err != nil {
return nil, nil, err
}
if err := build.ValidateLocalExportDelete(outputs, allowLocalOutputDelete); err != nil {
return nil, nil, err
}
opts.Annotations, err = buildflags.ParseAnnotations(in.Annotations)
if err != nil {
return nil, nil, errors.Wrap(err, "parse annotations")
@@ -1159,10 +1167,6 @@ func RunBuild(ctx context.Context, dockerCli command.Cli, in *BuildOptions, inSt
opts.SourcePolicy = in.SourcePolicy
opts.Policy = in.Policy
allow, err := buildflags.ParseEntitlements(in.Allow)
if err != nil {
return nil, nil, err
}
opts.Allow = allow
if in.CallFunc != nil {
+4
View File
@@ -901,6 +901,10 @@ target "default" {
}
```
> [!NOTE]
> Local outputs with `mode=delete` require granting `--allow=buildx.local.delete`
> when invoking `docker buildx bake`.
### `target.policy`
Policies to validate build sources and metadata. Each entry uses the same keys
+3
View File
@@ -85,6 +85,9 @@ The `fs` entitlements take a path value (relative or absolute) to a directory
on the filesystem. Alternatively, you can pass a wildcard (`*`) to allow Bake
to access the entire filesystem.
Bake also supports `--allow=buildx.local.delete` to grant local outputs
permission to delete stale files when `mode=delete` is set.
### Example: fs.read
Given the following Bake configuration, Bake would need to access the parent
+13 -3
View File
@@ -16,7 +16,7 @@ Start a build
| Name | Type | Default | Description |
|:----------------------------------------|:--------------|:----------|:-------------------------------------------------------------------------------------------------------------------------------------------------|
| [`--add-host`](#add-host) | `stringSlice` | | Add a custom host-to-IP mapping (format: `host:ip`) |
| [`--allow`](#allow) | `stringArray` | | Allow extra privileged entitlement (e.g., `network.host`, `security.insecure`, `device`) |
| [`--allow`](#allow) | `stringArray` | | Allow extra privileged entitlement (e.g., `network.host`, `security.insecure`, `device`, `buildx.local.delete`) |
| [`--annotation`](#annotation) | `stringArray` | | Add annotation to the image |
| [`--attest`](#attest) | `stringArray` | | Attestation parameters (format: `type=sbom,generator=image`) |
| [`--build-arg`](#build-arg) | `stringArray` | | Set build-time variables |
@@ -179,9 +179,14 @@ Allow extra privileged entitlement. List of entitlements:
- `--allow device` - Grants access to all devices.
- `--allow device=kind|name` - Grants access to a specific device.
- `--allow device=kind|name,alias=kind|name` - Grants access to a specific device, with optional aliasing.
- `buildx.local.delete` - Allows local outputs using `mode=delete` to delete
stale destination files when the destination is the current working directory
or outside it.
For entitlements to be enabled, the BuildKit daemon also needs to allow them
with `--allow-insecure-entitlement` (see [`create --buildkitd-flags`](buildx_create.md#buildkitd-flags)).
For BuildKit entitlements to be enabled, the BuildKit daemon also needs to allow
them with `--allow-insecure-entitlement` (see [`create --buildkitd-flags`](buildx_create.md#buildkitd-flags)).
The `buildx.local.delete` entitlement is checked by Buildx and isn't sent to the
BuildKit daemon.
```console
$ docker buildx create --use --name insecure-builder --buildkitd-flags '--allow-insecure-entitlement security.insecure'
@@ -753,6 +758,11 @@ will be put in subdirectories by their platform.
Attribute key:
- `dest` - destination directory where files will be written
- `mode` - write mode, either `copy` or `delete`. The default is `copy`.
`delete` removes stale files from the destination after exporting the build
result. It can be used without `--allow` when `dest` resolves to a
subdirectory of the current working directory. If `dest` is the current working
directory or resolves outside it, pass `--allow=buildx.local.delete`.
For more information, see
[Local and tar exporters](https://docs.docker.com/build/exporters/local-tar/).
+1 -1
View File
@@ -8,7 +8,7 @@ Start a build
| Name | Type | Default | Description |
|:--------------------|:--------------|:----------|:-------------------------------------------------------------------------------------------------------------------------------------------------|
| `--add-host` | `stringSlice` | | Add a custom host-to-IP mapping (format: `host:ip`) |
| `--allow` | `stringArray` | | Allow extra privileged entitlement (e.g., `network.host`, `security.insecure`, `device`) |
| `--allow` | `stringArray` | | Allow extra privileged entitlement (e.g., `network.host`, `security.insecure`, `device`, `buildx.local.delete`) |
| `--annotation` | `stringArray` | | Add annotation to the image |
| `--attest` | `stringArray` | | Attestation parameters (format: `type=sbom,generator=image`) |
| `--build-arg` | `stringArray` | | Set build-time variables |
+1 -2
View File
@@ -12,7 +12,7 @@ Start a build
| Name | Type | Default | Description |
|:--------------------|:--------------|:----------|:-------------------------------------------------------------------------------------------------------------------------------------------------|
| `--add-host` | `stringSlice` | | Add a custom host-to-IP mapping (format: `host:ip`) |
| `--allow` | `stringArray` | | Allow extra privileged entitlement (e.g., `network.host`, `security.insecure`, `device`) |
| `--allow` | `stringArray` | | Allow extra privileged entitlement (e.g., `network.host`, `security.insecure`, `device`, `buildx.local.delete`) |
| `--annotation` | `stringArray` | | Add annotation to the image |
| `--attest` | `stringArray` | | Attestation parameters (format: `type=sbom,generator=image`) |
| `--build-arg` | `stringArray` | | Set build-time variables |
@@ -51,4 +51,3 @@ Start a build
<!---MARKER_GEN_END-->
+25 -25
View File
@@ -1,6 +1,6 @@
module github.com/docker/buildx
go 1.25.5
go 1.25.9
require (
github.com/Masterminds/semver/v3 v3.4.0
@@ -10,10 +10,10 @@ require (
github.com/compose-spec/compose-go/v2 v2.10.2
github.com/containerd/console v1.0.5
github.com/containerd/containerd/v2 v2.2.4
github.com/containerd/continuity v0.4.5
github.com/containerd/continuity v0.5.0
github.com/containerd/errdefs v1.0.0
github.com/containerd/log v0.1.0
github.com/containerd/platforms v1.0.0-rc.2
github.com/containerd/platforms v1.0.0-rc.4
github.com/creack/pty v1.1.24
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/distribution/reference v0.6.0
@@ -30,7 +30,7 @@ require (
github.com/hashicorp/hcl/v2 v2.24.0
github.com/in-toto/in-toto-golang v0.11.0
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/moby/buildkit v0.30.0-rc2.0.20260610003221-41c29fffe299
github.com/moby/buildkit v0.30.0-rc2.0.20260610142556-f449174742bf
github.com/moby/go-archive v0.2.0
github.com/moby/moby/api v1.54.2
github.com/moby/moby/client v0.4.1
@@ -41,7 +41,7 @@ require (
github.com/open-policy-agent/opa v1.10.1
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
github.com/pelletier/go-toml/v2 v2.2.4
github.com/pelletier/go-toml/v2 v2.3.1
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/pkg/errors v0.9.1
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10
@@ -51,25 +51,25 @@ require (
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/tonistiigi/fsutil v0.0.0-20260603212341-ed540e182f8a
github.com/tonistiigi/fsutil v0.0.0-20260609091201-0257b3308df4
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0
github.com/tonistiigi/jaeger-ui-rest v0.0.0-20250408171107-3dd17559e117
github.com/zclconf/go-cty v1.17.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0
go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0
go.opentelemetry.io/otel v1.44.0
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0
go.opentelemetry.io/otel/metric v1.43.0
go.opentelemetry.io/otel/sdk v1.43.0
go.opentelemetry.io/otel/trace v1.43.0
go.opentelemetry.io/otel/metric v1.44.0
go.opentelemetry.io/otel/sdk v1.44.0
go.opentelemetry.io/otel/trace v1.44.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.52.0
golang.org/x/mod v0.35.0
golang.org/x/mod v0.36.0
golang.org/x/sync v0.20.0
golang.org/x/sys v0.45.0
golang.org/x/term v0.43.0
golang.org/x/text v0.37.0
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d
google.golang.org/grpc v1.80.0
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa
google.golang.org/grpc v1.81.1
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1
google.golang.org/protobuf v1.36.11
k8s.io/api v0.35.4
@@ -148,7 +148,7 @@ require (
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-containerregistry v0.20.7 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hiddeco/sshsig v0.2.0 // indirect
github.com/in-toto/attestation v1.1.2 // indirect
@@ -212,22 +212,22 @@ require (
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
github.com/yashtewari/glob-intersection v0.2.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.68.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
+58 -56
View File
@@ -122,8 +122,8 @@ github.com/containerd/containerd/api v1.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXe
github.com/containerd/containerd/api v1.10.0/go.mod h1:NBm1OAk8ZL+LG8R0ceObGxT5hbUYj7CzTmR3xh0DlMM=
github.com/containerd/containerd/v2 v2.2.4 h1:8x2UdXqww7NYqGNabQ7i1nAgB5LegzjC9KQzO/900iA=
github.com/containerd/containerd/v2 v2.2.4/go.mod h1:YBcTO8D9149QY9zNmUjy04Mhuc4DlrZQ8FIOwKZEM7o=
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg=
github.com/containerd/continuity v0.5.0/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
@@ -132,10 +132,10 @@ github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY
github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=
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/nydus-snapshotter v0.15.13 h1:z9yCiTPMxVBIZlHxOPinZXhly2MdcIqxk9VXPlHIOJY=
github.com/containerd/nydus-snapshotter v0.15.13/go.mod h1:t95dwCb4I0RE4n1iOk0sJCWosNoACA8daOXmU5A2VHI=
github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4=
github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4=
github.com/containerd/nydus-snapshotter v0.15.15 h1:kVYbFpYA4K43qxGVoc/VBwRXLAVWn4X9mdwGrR+HsLk=
github.com/containerd/nydus-snapshotter v0.15.15/go.mod h1:L96yO+4iE6qqDiqXKhxMXBoPeaE7JgzXir9yanUVuOY=
github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4=
github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A=
github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y=
github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8=
github.com/containerd/stargz-snapshotter v0.18.2 h1:Ev/sxfQUjwzJQ9eqy3XzttcQ3osMIqkQgMYlcET+10M=
@@ -321,8 +321,8 @@ github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5T
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
@@ -415,8 +415,8 @@ github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/buildkit v0.30.0-rc2.0.20260610003221-41c29fffe299 h1:vDHdjaWsBBAgG3Y59MTsTxzxmGEwvmod1AG5tb8mzwk=
github.com/moby/buildkit v0.30.0-rc2.0.20260610003221-41c29fffe299/go.mod h1:lWVt4gAu/Erz7leTk63nz+FbnZy8VPTgbFGWteapT60=
github.com/moby/buildkit v0.30.0-rc2.0.20260610142556-f449174742bf h1:9qGH4EXFQgmqJUMjRxyX0zxA5L2x7CJiNHPAksuaF+U=
github.com/moby/buildkit v0.30.0-rc2.0.20260610142556-f449174742bf/go.mod h1:9NQbhPKN8TizS1PFglCTHotCdHXF3Vm36FR5t3fjJ38=
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.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
@@ -471,13 +471,13 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg=
github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/selinux v1.14.1 h1:a7XlXV/nN/l5zFP1FWZYoExpClu1QOPMfWUV2CZ8kEQ=
github.com/opencontainers/selinux v1.14.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ=
github.com/opencontainers/selinux v1.15.1 h1:ERxeh5caJvCzNAKdI8WQbJmB1LDTn4BuaAg8wihLBpA=
github.com/opencontainers/selinux v1.15.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ=
github.com/package-url/packageurl-go v0.1.1 h1:KTRE0bK3sKbFKAk3yy63DpeskU7Cvs/x/Da5l+RtzyU=
github.com/package-url/packageurl-go v0.1.1/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -574,8 +574,8 @@ github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 h1:r0p7fK56l8WPequOaR3i9LBqfPtEdXIQbUTzT55iqT4=
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY=
github.com/tonistiigi/fsutil v0.0.0-20260603212341-ed540e182f8a h1:fouZioB486i05IyehzLIrq4Jcz5LHQbMJsznx6ByJZo=
github.com/tonistiigi/fsutil v0.0.0-20260603212341-ed540e182f8a/go.mod h1:BKdcez7BiVtBvIcef90ZPc6ebqIWr4JWD7+EvLm6J98=
github.com/tonistiigi/fsutil v0.0.0-20260609091201-0257b3308df4 h1:tJkv/edHw9FXVtbHxc6cpqDttiCLNzhqI1W40fcnxIY=
github.com/tonistiigi/fsutil v0.0.0-20260609091201-0257b3308df4/go.mod h1:K5zrLch9UaSGNiek5XHZeqZUf1zPWJHqDfLIcnpquQ4=
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 h1:2f304B10LaZdB8kkVEaoXvAMVan2tl9AiK4G0odjQtE=
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE=
github.com/tonistiigi/jaeger-ui-rest v0.0.0-20250408171107-3dd17559e117 h1:XFwyh2JZwR5aiKLXHX2C1n0v5F11dCJpyGL1W/Cpl3U=
@@ -590,8 +590,8 @@ github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG
github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=
github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
github.com/vbatts/tar-split v0.12.3 h1:Cd46rkGXI3Td4yrVNwU8ripbxFaQbmesqhjBUUYAJSw=
github.com/vbatts/tar-split v0.12.3/go.mod h1:sQOc6OlqGCr7HkGx/IDBeKiTIvqhmj8KffNhEXG4Nq0=
github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE=
github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
@@ -614,34 +614,36 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.68.0 h1:cuXaPAfIoJKsYjBjPSb2nKZEmgM43zVr25l37IxhKME=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.68.0/go.mod h1:BuzhPofpCzlDi/Q/Xjg54M4/3oWqqyDe2Zeq7A2I0QE=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 h1:2yEATaop1/a1I4psnSLgWVPLWwCzkqWakgJy7xTDVy0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0/go.mod h1:D7J12YRapIekYyPWgGPlA/23pRmpSEZC5xJC/TTLI9U=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0 h1:MCcYL7J6Vt/X0kjqbMZkekCmwsurbQRbL69vkiye2lk=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0/go.mod h1:3jnStNwSufK+f5ktjL4EPcwtig4rtd81NS70lqHuXl8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0 h1:SUplec5dp06reu1zaXmOXdvqH398taqrDXqUl99jxSc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0/go.mod h1:ho2g4N+ane+swq5I/VBkKWnRDY4kUINH3FuqyZqX/Ug=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 h1:RuynHbfU8JUEw7DyONgkVYg2SVtsoF28y0LGIr69jgA=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0/go.mod h1:qZF+/lBs71APw8mlnEZcqZHMzqrYrsFiJOv83lX1OGo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.step.sm/crypto v0.77.2 h1:qFjjei+RHc5kP5R7NW9OUWT7SqWIuAOvOkXqg4fNWj8=
@@ -652,18 +654,18 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U=
go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/exp v0.0.0-20260603202125-055de637280b h1:v1uXiEBHo8QA0LiGCo7UgHMzHT4Kdfpl2zmtH5vaP1Q=
golang.org/x/exp v0.0.0-20260603202125-055de637280b/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
@@ -688,12 +690,12 @@ google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA=
google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA=
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE=
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw=
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI=
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+75
View File
@@ -45,6 +45,7 @@ var bakeTests = []func(t *testing.T, sb integration.Sandbox){
testBakePrintRemoteContextSubdir,
testBakeLocal,
testBakeLocalMulti,
testBakeLocalExportDeleteMode,
testBakeRemote,
testBakeRemoteAuth,
testBakeRemoteCmdContext,
@@ -673,6 +674,80 @@ services:
require.FileExists(t, filepath.Join(dirDest2, "foo"))
}
func testBakeLocalExportDeleteMode(t *testing.T, sb integration.Sandbox) {
dockerfile := []byte(`
FROM scratch
COPY foo /foo
`)
bakefile := []byte(`
target "default" {
output = ["type=local,dest=out,mode=delete"]
}
`)
t.Run("definition requires allow", func(t *testing.T) {
dir := tmpdir(
t,
fstest.CreateFile("docker-bake.hcl", bakefile, 0600),
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("foo", []byte("foo"), 0600),
)
cmd := buildxCmd(sb, withDir(dir), withArgs("bake", "--progress=rawjson"))
out, err := cmd.CombinedOutput()
require.Error(t, err, string(out))
require.Contains(t, string(out), "--allow=buildx.local.delete")
})
t.Run("set requires allow", func(t *testing.T) {
dir := tmpdir(
t,
fstest.CreateFile("docker-bake.hcl", []byte(`target "default" {}`), 0600),
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("foo", []byte("foo"), 0600),
)
cmd := buildxCmd(sb, withDir(dir), withArgs("bake", "--progress=rawjson", "--set", "*.output=type=local,dest=out,mode=delete"))
out, err := cmd.CombinedOutput()
require.Error(t, err, string(out))
require.Contains(t, string(out), "--allow=buildx.local.delete")
})
t.Run("allow does not accept value", func(t *testing.T) {
dir := tmpdir(
t,
fstest.CreateFile("docker-bake.hcl", bakefile, 0600),
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("foo", []byte("foo"), 0600),
)
out, err := bakeCmd(sb, withDir(dir), withArgs("--allow=buildx.local.delete=true"))
require.Error(t, err, out)
require.Contains(t, out, "buildx.local.delete does not accept a value")
})
t.Run("allow deletes stale files", func(t *testing.T) {
skipNoCompatBuildKit(t, sb, ">= 0.31.0-0", "local exporter mode=delete")
dir := tmpdir(
t,
fstest.CreateFile("docker-bake.hcl", bakefile, 0600),
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("foo", []byte("foo"), 0600),
)
dest := filepath.Join(dir, "out")
stale := filepath.Join(dest, "stale")
require.NoError(t, os.MkdirAll(dest, 0o755))
require.NoError(t, os.WriteFile(stale, []byte("stale"), 0o600))
out, err := bakeCmd(sb, withDir(dir), withArgs("--allow=buildx.local.delete"))
require.NoError(t, err, out)
require.FileExists(t, filepath.Join(dest, "foo"))
_, err = os.Stat(stale)
require.ErrorIs(t, err, os.ErrNotExist)
})
}
func testBakeRemote(t *testing.T, sb integration.Sandbox) {
bakefile := []byte(`
target "default" {
+64
View File
@@ -63,6 +63,7 @@ var buildTests = []func(t *testing.T, sb integration.Sandbox){
testBuildLocalStateRemote,
testImageIDOutput,
testBuildLocalExport,
testBuildLocalExportDeleteMode,
testBuildRegistryExport,
testBuildRegistryExportAttestations,
testBuildTarExport,
@@ -491,6 +492,69 @@ func testBuildLocalExport(t *testing.T, sb integration.Sandbox) {
require.Equal(t, "foo", string(dt))
}
func testBuildLocalExportDeleteMode(t *testing.T, sb integration.Sandbox) {
t.Run("requires allow for current working directory", func(t *testing.T) {
dir := createTestProject(t)
out, err := buildCmd(sb, withDir(dir), withArgs("--output=type=local,dest=.,mode=delete", "."))
require.Error(t, err, out)
require.Contains(t, out, "--allow=buildx.local.delete")
})
t.Run("requires allow for outside destination", func(t *testing.T) {
dir := createTestProject(t)
dest := filepath.Join(t.TempDir(), "out")
out, err := buildCmd(sb, withDir(dir), withArgs("--output=type=local,dest="+dest+",mode=delete", "."))
require.Error(t, err, out)
require.Contains(t, out, "--allow=buildx.local.delete")
})
t.Run("requires allow for symlink outside destination", func(t *testing.T) {
dir := createTestProject(t)
require.NoError(t, os.Symlink(t.TempDir(), filepath.Join(dir, "out")))
out, err := buildCmd(sb, withDir(dir), withArgs("--output=type=local,dest=out,mode=delete", "."))
require.Error(t, err, out)
require.Contains(t, out, "--allow=buildx.local.delete")
})
t.Run("allow does not accept value", func(t *testing.T) {
dir := createTestProject(t)
out, err := buildCmd(sb, withDir(dir), withArgs("--allow=buildx.local.delete=true", "--output=type=local,dest=out", "."))
require.Error(t, err, out)
require.Contains(t, out, "buildx.local.delete does not accept a value")
})
t.Run("deletes stale files in subdirectory without allow", func(t *testing.T) {
skipNoCompatBuildKit(t, sb, ">= 0.31.0-0", "local exporter mode=delete")
dir := createTestProject(t)
dest := filepath.Join(dir, "out")
stale := filepath.Join(dest, "stale")
require.NoError(t, os.MkdirAll(dest, 0o755))
require.NoError(t, os.WriteFile(stale, []byte("stale"), 0o600))
out, err := buildCmd(sb, withDir(dir), withArgs("--output=type=local,dest=out,mode=delete", "."))
require.NoError(t, err, out)
require.FileExists(t, filepath.Join(dest, "bar"))
_, err = os.Stat(stale)
require.ErrorIs(t, err, os.ErrNotExist)
})
t.Run("allow permits current working directory", func(t *testing.T) {
skipNoCompatBuildKit(t, sb, ">= 0.31.0-0", "local exporter mode=delete")
dir := createTestProject(t)
stale := filepath.Join(dir, "stale")
require.NoError(t, os.WriteFile(stale, []byte("stale"), 0o600))
out, err := buildCmd(sb, withDir(dir), withArgs("--allow=buildx.local.delete", "--output=type=local,dest=.,mode=delete", "."))
require.NoError(t, err, out)
require.FileExists(t, filepath.Join(dir, "bar"))
_, err = os.Stat(stale)
require.ErrorIs(t, err, os.ErrNotExist)
})
}
func testBuildTarExport(t *testing.T, sb integration.Sandbox) {
dir := createTestProject(t)
outdir := path.Join(dir, "out")
+16 -4
View File
@@ -1,20 +1,32 @@
package buildflags
import (
"strings"
"github.com/moby/buildkit/util/entitlements"
"github.com/pkg/errors"
)
func ParseEntitlements(in []string) ([]string, error) {
const EntitlementBuildxLocalDelete = "buildx.local.delete"
func ParseEntitlements(in []string) (_ []string, allowLocalOutputDelete bool, _ error) {
out := make([]string, 0, len(in))
for _, v := range in {
if v == "" {
continue
}
k, _, hasValue := strings.Cut(v, "=")
if k == EntitlementBuildxLocalDelete {
if hasValue {
return nil, false, errors.Errorf("%s does not accept a value", EntitlementBuildxLocalDelete)
}
allowLocalOutputDelete = true
continue
}
if _, _, err := entitlements.Parse(v); err != nil {
return nil, err
return nil, false, err
}
out = append(out, v)
}
return out, nil
return out, allowLocalOutputDelete, nil
}
+122
View File
@@ -1,8 +1,11 @@
package osutil
import (
"errors"
"io/fs"
"os"
"path/filepath"
"syscall"
)
// GetWd retrieves the current working directory.
@@ -28,3 +31,122 @@ func ToAbs(path string) string {
}
return SanitizePath(path)
}
func EvaluateToExistingPath(in string) (string, string, error) {
in, err := filepath.Abs(in)
if err != nil {
return "", "", err
}
volLen := volumeNameLen(in)
pathSeparator := string(os.PathSeparator)
if volLen < len(in) && os.IsPathSeparator(in[volLen]) {
volLen++
}
vol := in[:volLen]
dest := vol
linksWalked := 0
var end int
for start := volLen; start < len(in); start = end {
for start < len(in) && os.IsPathSeparator(in[start]) {
start++
}
end = start
for end < len(in) && !os.IsPathSeparator(in[end]) {
end++
}
if end == start {
break
} else if in[start:end] == "." {
continue
} else if in[start:end] == ".." {
var r int
for r = len(dest) - 1; r >= volLen; r-- {
if os.IsPathSeparator(dest[r]) {
break
}
}
if r < volLen || dest[r+1:] == ".." {
if len(dest) > volLen {
dest += pathSeparator
}
dest += ".."
} else {
dest = dest[:r]
}
continue
}
if len(dest) > volumeNameLen(dest) && !os.IsPathSeparator(dest[len(dest)-1]) {
dest += pathSeparator
}
dest += in[start:end]
fi, err := os.Lstat(dest)
if err != nil {
if os.IsNotExist(err) {
for r := len(dest) - 1; r >= volLen; r-- {
if os.IsPathSeparator(dest[r]) {
return dest[:r], in[start:], nil
}
}
return vol, in[start:], nil
}
return "", "", err
}
if fi.Mode()&fs.ModeSymlink == 0 {
if !fi.Mode().IsDir() && end < len(in) {
return "", "", syscall.ENOTDIR
}
continue
}
linksWalked++
if linksWalked > 255 {
return "", "", errors.New("too many symlinks")
}
link, err := os.Readlink(dest)
if err != nil {
return "", "", err
}
in = link + in[end:]
v := volumeNameLen(link)
if v > 0 {
if v < len(link) && os.IsPathSeparator(link[v]) {
v++
}
vol = link[:v]
dest = vol
end = len(vol)
} else if len(link) > 0 && os.IsPathSeparator(link[0]) {
dest = link[:1]
end = 1
vol = link[:1]
volLen = 1
} else {
var r int
for r = len(dest) - 1; r >= volLen; r-- {
if os.IsPathSeparator(dest[r]) {
break
}
}
if r < volLen {
dest = vol
} else {
dest = dest[:r]
}
end = 0
}
}
return filepath.Clean(dest), "", nil
}
func volumeNameLen(s string) int {
return len(filepath.VolumeName(s))
}
+1 -1
View File
@@ -216,7 +216,7 @@ func (c *context) verifyMetadata(resource, target Resource) error {
}
if target.GID() != resource.GID() {
return fmt.Errorf("unexpected gid for %q: %v != %v", target.Path(), target.GID(), target.GID())
return fmt.Errorf("unexpected gid for %q: %v != %v", target.Path(), target.GID(), resource.GID())
}
if xattrer, ok := resource.(XAttrer); ok {
+2 -2
View File
@@ -38,14 +38,14 @@ func DeviceInfo(fi os.FileInfo) (uint64, uint64, error) {
}
// mknod provides a shortcut for syscall.Mknod
func Mknod(p string, mode os.FileMode, maj, min int) error {
func Mknod(p string, mode os.FileMode, major, minor int) error {
var (
m = syscallMode(mode.Perm())
dev uint64
)
if mode&os.ModeDevice != 0 {
dev = unix.Mkdev(uint32(maj), uint32(min))
dev = unix.Mkdev(uint32(major), uint32(minor))
if mode&os.ModeCharDevice != 0 {
m |= unix.S_IFCHR
+1 -1
View File
@@ -109,7 +109,7 @@ type LXAttrDriver interface {
}
type DeviceInfoDriver interface {
DeviceInfo(fi os.FileInfo) (maj uint64, min uint64, err error)
DeviceInfo(fi os.FileInfo) (major uint64, minor uint64, err error)
}
// driver is a simple default implementation that sends calls out to the "os"
+1 -1
View File
@@ -128,6 +128,6 @@ func (d *driver) LSetxattr(path string, attrMap map[string][]byte) error {
return nil
}
func (d *driver) DeviceInfo(fi os.FileInfo) (maj uint64, min uint64, err error) {
func (d *driver) DeviceInfo(fi os.FileInfo) (major uint64, minor uint64, err error) {
return devices.DeviceInfo(fi)
}
+116
View File
@@ -26,6 +26,122 @@ import (
"golang.org/x/sys/unix"
)
// maxCopyChunk is the maximum size passed to copy_file_range per call,
// avoiding int overflow on 32-bit architectures.
const maxCopyChunk = 1 << 30 // 1 GiB
// copyFile copies a file from source to target preserving sparse file holes.
//
// If the filesystem does not support SEEK_DATA/SEEK_HOLE, it falls back
// to a plain io.Copy.
func copyFile(target, source string) error {
src, err := os.Open(source)
if err != nil {
return fmt.Errorf("failed to open source %s: %w", source, err)
}
defer src.Close()
fi, err := src.Stat()
if err != nil {
return fmt.Errorf("failed to stat source %s: %w", source, err)
}
size := fi.Size()
tgt, err := os.Create(target)
if err != nil {
return fmt.Errorf("failed to open target %s: %w", target, err)
}
defer tgt.Close()
if err := tgt.Truncate(size); err != nil {
return fmt.Errorf("failed to truncate target %s: %w", target, err)
}
srcFd := int(src.Fd())
// Try a SEEK_DATA to check if the filesystem supports it.
// If not, fall back to a plain copy.
if _, err := unix.Seek(srcFd, 0, unix.SEEK_DATA); err != nil {
// ENXIO means no data in the file at all. In other words it's entirely sparse.
// The truncated target is already correct.
if errors.Is(err, syscall.ENXIO) {
return nil
}
if errors.Is(err, syscall.EOPNOTSUPP) || errors.Is(err, syscall.ENOTSUP) || errors.Is(err, syscall.EINVAL) {
// Filesystem doesn't support SEEK_DATA/SEEK_HOLE. Fall back to a plain copy.
src.Close()
tgt.Close()
return openAndCopyFile(target, source)
}
return fmt.Errorf("failed to seek data in source %s: %w", source, err)
}
// Copy data regions from source to target, skipping holes.
var offset int64
tgtFd := int(tgt.Fd())
for offset < size {
dataStart, err := unix.Seek(srcFd, offset, unix.SEEK_DATA)
if err != nil {
// No more data past offset. Remainder of file is a hole.
if errors.Is(err, syscall.ENXIO) {
break
}
return fmt.Errorf("SEEK_DATA failed at offset %d: %w", offset, err)
}
// Find the end of this data region (start of next hole).
holeStart, err := unix.Seek(srcFd, dataStart, unix.SEEK_HOLE)
if err != nil {
// ENXIO shouldn't happen after a successful SEEK_DATA, but
// treat it as data extending to end of file.
if errors.Is(err, syscall.ENXIO) {
holeStart = size
} else {
return fmt.Errorf("SEEK_HOLE failed at offset %d: %w", dataStart, err)
}
}
// Copy the data region [dataStart, holeStart).
srcOff := dataStart
tgtOff := dataStart
remain := holeStart - dataStart
for remain > 0 {
chunk := remain
if chunk > maxCopyChunk {
chunk = maxCopyChunk
}
n, err := unix.CopyFileRange(srcFd, &srcOff, tgtFd, &tgtOff, int(chunk), 0)
if err != nil {
// Fall back to a plain copy if copy_file_range is not supported
// across the source and target filesystems.
if errors.Is(err, syscall.EXDEV) || errors.Is(err, syscall.ENOSYS) || errors.Is(err, syscall.EOPNOTSUPP) {
src.Close()
tgt.Close()
return openAndCopyFile(target, source)
}
return fmt.Errorf("copy_file_range failed: %w", err)
}
if n == 0 {
return fmt.Errorf("copy_file_range returned 0 with %d bytes remaining", remain)
}
remain -= int64(n)
}
offset = holeStart
}
if err := tgt.Sync(); err != nil {
return fmt.Errorf("failed to sync target %s: %w", target, err)
}
return nil
}
func copyFileInfo(fi os.FileInfo, src, name string) error {
st := fi.Sys().(*syscall.Stat_t)
if err := os.Lchown(name, int(st.Uid), int(st.Gid)); err != nil {
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build !darwin
//go:build !darwin && !linux
/*
Copyright The containerd Authors.
+2 -2
View File
@@ -51,11 +51,11 @@ func overlayFSWhiteoutConvert(diffDir, path string, f os.FileInfo, changeFn Chan
return false, nil
}
maj, min, err := devices.DeviceInfo(f)
major, minor, err := devices.DeviceInfo(f)
if err != nil {
return false, err
}
return (maj == 0 && min == 0), nil
return (major == 0 && minor == 0), nil
}
if f.IsDir() {
+4 -2
View File
@@ -20,11 +20,13 @@ package fs
import (
"bytes"
"errors"
"fmt"
"os"
"syscall"
"github.com/containerd/continuity/sysx"
"golang.org/x/sys/unix"
)
// compareSysStat returns whether the stats are equivalent,
@@ -45,11 +47,11 @@ func compareSysStat(s1, s2 interface{}) (bool, error) {
func compareCapabilities(p1, p2 string) (bool, error) {
c1, err := sysx.LGetxattr(p1, "security.capability")
if err != nil && err != sysx.ENODATA {
if err != nil && !errors.Is(err, unix.ENOTSUP) && !errors.Is(err, sysx.ENODATA) {
return false, fmt.Errorf("failed to get xattr for %s: %w", p1, err)
}
c2, err := sysx.LGetxattr(p2, "security.capability")
if err != nil && err != sysx.ENODATA {
if err != nil && !errors.Is(err, unix.ENOTSUP) && !errors.Is(err, sysx.ENODATA) {
return false, fmt.Errorf("failed to get xattr for %s: %w", p2, err)
}
return bytes.Equal(c1, c2), nil
+2 -2
View File
@@ -111,9 +111,9 @@ func CreateDir(name string, perm os.FileMode) Applier {
}
// Rename returns a file applier which renames a file
func Rename(old, new string) Applier {
func Rename(oldpath, newpath string) Applier {
return applyFn(func(root string) error {
return os.Rename(filepath.Join(root, old), filepath.Join(root, new))
return os.Rename(filepath.Join(root, oldpath), filepath.Join(root, newpath))
})
}
+2 -2
View File
@@ -48,10 +48,10 @@ func Lchtimes(name string, atime, mtime time.Time) Applier {
}
// CreateDeviceFile provides creates devices Applier.
func CreateDeviceFile(name string, mode os.FileMode, maj, min int) Applier {
func CreateDeviceFile(name string, mode os.FileMode, major, minor int) Applier {
return applyFn(func(root string) error {
fullPath := filepath.Join(root, name)
return devices.Mknod(fullPath, mode, maj, min)
return devices.Mknod(fullPath, mode, major, minor)
})
}
+1 -1
View File
@@ -37,7 +37,7 @@ func Lchtimes(name string, atime, mtime time.Time) Applier {
}
// CreateDeviceFile provides creates devices Applier.
func CreateDeviceFile(name string, mode os.FileMode, maj, min int) Applier {
func CreateDeviceFile(name string, mode os.FileMode, major, minor int) Applier {
return applyFn(func(root string) error {
return errors.New("Not implemented")
})
+15 -22
View File
@@ -1,32 +1,25 @@
version: "2"
linters:
enable:
- copyloopvar
- gofmt
- goimports
- dupword
- gosec
- ineffassign
- misspell
- nolintlint
- revive
- staticcheck
- tenv # Detects using os.Setenv instead of t.Setenv since Go 1.17
- unconvert
- unused
- govet
- dupword # Checks for duplicate words in the source code
disable:
- errcheck
run:
timeout: 5m
issues:
exclude-dirs:
- api
- cluster
- design
- docs
- docs/man
- releases
- reports
- test # e2e scripts
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
+105 -1
View File
@@ -152,6 +152,88 @@ func Only(platform specs.Platform) MatchComparer {
return Ordered(platformVector(Normalize(platform))...)
}
// OnlyOS returns a match comparer that matches only platforms with the same
// OS, OS version, and OS features, regardless of architecture. When comparing,
// it always ranks the best architecture match highest using the default
// platform resolution logic.
func OnlyOS(platform specs.Platform) MatchComparer {
normalized := Normalize(platform)
return onlyOSComparer{
platform: normalized,
osvM: newOSVersionMatcher(normalized),
archOrder: orderedPlatformComparer{
matchers: []Matcher{NewMatcher(normalized)},
},
}
}
func newOSVersionMatcher(platform specs.Platform) osVerMatcher {
if platform.OS == "windows" {
return &windowsVersionMatcher{
windowsOSVersion: getWindowsOSVersion(platform.OSVersion),
}
}
return nil
}
type onlyOSComparer struct {
platform specs.Platform
osvM osVerMatcher
archOrder orderedPlatformComparer
}
func (c onlyOSComparer) matchOS(platform specs.Platform) bool {
normalized := Normalize(platform)
if c.platform.OS != normalized.OS {
return false
}
if c.osvM != nil {
if !c.osvM.Match(platform.OSVersion) {
return false
}
}
if len(normalized.OSFeatures) > 0 {
if len(c.platform.OSFeatures) < len(normalized.OSFeatures) {
return false
}
j := 0
for _, feature := range normalized.OSFeatures {
found := false
for ; j < len(c.platform.OSFeatures); j++ {
if feature == c.platform.OSFeatures[j] {
found = true
j++
break
}
if feature < c.platform.OSFeatures[j] {
return false
}
}
if !found {
return false
}
}
}
return true
}
func (c onlyOSComparer) Match(platform specs.Platform) bool {
return c.matchOS(platform)
}
func (c onlyOSComparer) Less(p1, p2 specs.Platform) bool {
p1m := c.matchOS(p1)
p2m := c.matchOS(p2)
if p1m && !p2m {
return true
}
if !p1m {
return false
}
// Both match — rank by architecture preference
return c.archOrder.Less(p1, p2)
}
// OnlyStrict returns a match comparer for a single platform.
//
// Unlike Only, OnlyStrict does not match sub platforms.
@@ -213,9 +295,20 @@ func (c orderedPlatformComparer) Less(p1 specs.Platform, p2 specs.Platform) bool
return true
}
if p1m || p2m {
if p1m && p2m {
// Prefer one with most matching features
if len(p1.OSFeatures) != len(p2.OSFeatures) {
return len(p1.OSFeatures) > len(p2.OSFeatures)
}
}
return false
}
}
if len(p1.OSFeatures) > 0 || len(p2.OSFeatures) > 0 {
p1.OSFeatures = nil
p2.OSFeatures = nil
return c.Less(p1, p2)
}
return false
}
@@ -242,9 +335,20 @@ func (c anyPlatformComparer) Less(p1, p2 specs.Platform) bool {
p2m = true
}
if p1m && p2m {
return false
if len(p1.OSFeatures) != len(p2.OSFeatures) {
return len(p1.OSFeatures) > len(p2.OSFeatures)
}
break
}
}
// If neither match and has features, strip features and compare
if !p1m && !p2m && (len(p1.OSFeatures) > 0 || len(p2.OSFeatures) > 0) {
p1.OSFeatures = nil
p2.OSFeatures = nil
return c.Less(p1, p2)
}
// If one matches, and the other does, sort match first
return p1m && !p2m
}
-2
View File
@@ -45,7 +45,6 @@ func getMachineArch() (string, error) {
// So we don't need to access the ARM registers to detect platform information
// by ourselves. We can just parse these information from /proc/cpuinfo
func getCPUInfo(pattern string) (info string, err error) {
cpuinfo, err := os.Open("/proc/cpuinfo")
if err != nil {
return "", err
@@ -75,7 +74,6 @@ func getCPUInfo(pattern string) (info string, err error) {
// getCPUVariantFromArch get CPU variant from arch through a system call
func getCPUVariantFromArch(arch string) (string, error) {
var variant string
arch = strings.ToLower(arch)
+4 -4
View File
@@ -24,10 +24,10 @@ import (
)
func getCPUVariant() (string, error) {
var variant string
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
switch runtime.GOOS {
case "windows", "darwin":
// Windows/Darwin only supports v7 for ARM32 and v8 for ARM64 and so we can use
// runtime.GOARCH to determine the variants
switch runtime.GOARCH {
@@ -38,7 +38,7 @@ func getCPUVariant() (string, error) {
default:
variant = "unknown"
}
} else if runtime.GOOS == "freebsd" {
case "freebsd":
// FreeBSD supports ARMv6 and ARMv7 as well as ARMv4 and ARMv5 (though deprecated)
// detecting those variants is currently unimplemented
switch runtime.GOARCH {
@@ -47,7 +47,7 @@ func getCPUVariant() (string, error) {
default:
variant = "unknown"
}
} else {
default:
return "", fmt.Errorf("getCPUVariant for OS %s: %v", runtime.GOOS, errNotImplemented)
}
+12
View File
@@ -17,6 +17,7 @@
package platforms
import (
"slices"
"strconv"
"strings"
@@ -162,3 +163,14 @@ func (c *windowsMatchComparer) Less(p1, p2 specs.Platform) bool {
}
return m1 && !m2
}
type windowsStripFeaturesMatcher struct {
Matcher
}
func (m windowsStripFeaturesMatcher) Match(p specs.Platform) bool {
if i := slices.Index(p.OSFeatures, "win32k"); i >= 0 {
p.OSFeatures = slices.Delete(slices.Clone(p.OSFeatures), i, i+1)
}
return m.Matcher.Match(p)
}
+167 -21
View File
@@ -111,9 +111,11 @@ package platforms
import (
"fmt"
"net/url"
"path"
"regexp"
"runtime"
"slices"
"strconv"
"strings"
@@ -122,11 +124,9 @@ import (
var (
specifierRe = regexp.MustCompile(`^[A-Za-z0-9_.-]+$`)
osAndVersionRe = regexp.MustCompile(`^([A-Za-z0-9_-]+)(?:\(([A-Za-z0-9_.-]*)\))?$`)
osRe = regexp.MustCompile(`^([A-Za-z0-9_-]+)(?:\(([A-Za-z0-9_.%-]*)((?:\+[A-Za-z0-9_.%-]+)*)\))?$`)
)
const osAndVersionFormat = "%s(%s)"
// Platform is a type alias for convenience, so there is no need to import image-spec package everywhere.
type Platform = specs.Platform
@@ -143,6 +143,10 @@ type Matcher interface {
// functionality.
//
// Applications should opt to use `Match` over directly parsing specifiers.
//
// For OSFeatures, this matcher will match if the platform to match has
// OSFeatures which are a subset of the OSFeatures of the platform
// provided to NewMatcher.
func NewMatcher(platform specs.Platform) Matcher {
m := &matcher{
Platform: Normalize(platform),
@@ -152,6 +156,11 @@ func NewMatcher(platform specs.Platform) Matcher {
m.osvM = &windowsVersionMatcher{
windowsOSVersion: getWindowsOSVersion(platform.OSVersion),
}
// In prior versions, the win32k os feature was not considered for matching,
// strip out the win32k feature for comparison
var stripped Matcher = windowsStripFeaturesMatcher{m}
// In prior versions, on windows, the returned matcher implements a
// MatchComprarer interface.
// This preserves that behavior for backwards compatibility.
@@ -161,8 +170,9 @@ func NewMatcher(platform specs.Platform) Matcher {
// It was likely intended to be used in `Ordered` but it is not since
// `Less` that is implemented here ends up getting masked due to wrapping.
if runtime.GOOS == "windows" {
return &windowsMatchComparer{m}
return &windowsMatchComparer{stripped}
}
return stripped
}
return m
}
@@ -178,10 +188,39 @@ type matcher struct {
func (m *matcher) Match(platform specs.Platform) bool {
normalized := Normalize(platform)
return m.OS == normalized.OS &&
if m.OS == normalized.OS &&
m.Architecture == normalized.Architecture &&
m.Variant == normalized.Variant &&
m.matchOSVersion(platform)
m.matchOSVersion(platform) {
if len(normalized.OSFeatures) == 0 {
return true
}
if len(m.OSFeatures) >= len(normalized.OSFeatures) {
// Ensure that normalized.OSFeatures is a subset of
// m.OSFeatures
j := 0
for _, feature := range normalized.OSFeatures {
found := false
for ; j < len(m.OSFeatures); j++ {
if feature == m.OSFeatures[j] {
found = true
j++
break
}
// Since both lists are ordered, if the feature is less
// than what is seen, it is not in the list
if feature < m.OSFeatures[j] {
return false
}
}
if !found {
return false
}
}
return true
}
}
return false
}
func (m *matcher) matchOSVersion(platform specs.Platform) bool {
@@ -210,11 +249,14 @@ func ParseAll(specifiers []string) ([]specs.Platform, error) {
// Parse parses the platform specifier syntax into a platform declaration.
//
// Platform specifiers are in the format `<os>[(<OSVersion>)]|<arch>|<os>[(<OSVersion>)]/<arch>[/<variant>]`.
// Platform specifiers are in the format `<os>[(<os options>)]|<arch>|<os>[(<os options>)]/<arch>[/<variant>]`.
// The minimum required information for a platform specifier is the operating
// system or architecture. The OSVersion can be part of the OS like `windows(10.0.17763)`
// When an OSVersion is specified, then specs.Platform.OSVersion is populated with that value,
// and an empty string otherwise.
// system or architecture. The "os options" may be OSVersion which can be part of the OS
// like `windows(10.0.17763)`. When an OSVersion is specified, then specs.Platform.OSVersion is
// populated with that value, and an empty string otherwise. The "os options" may also include an
// array of OSFeatures, each feature prefixed with '+', without any other separator, and provided
// after the OSVersion when the OSVersion is specified. An "os options" with version and features
// is like `windows(10.0.17763+win32k)`.
// If there is only a single string (no slashes), the
// value will be matched against the known set of operating systems, then fall
// back to the known set of architectures. The missing component will be
@@ -231,14 +273,24 @@ func Parse(specifier string) (specs.Platform, error) {
var p specs.Platform
for i, part := range parts {
if i == 0 {
// First element is <os>[(<OSVersion>)]
osVer := osAndVersionRe.FindStringSubmatch(part)
if osVer == nil {
return specs.Platform{}, fmt.Errorf("%q is an invalid OS component of %q: OSAndVersion specifier component must match %q: %w", part, specifier, osAndVersionRe.String(), errInvalidArgument)
// First element is <os>[(<OSVersion>[+<OSFeature>]*)]
osOptions := osRe.FindStringSubmatch(part)
if osOptions == nil {
return specs.Platform{}, fmt.Errorf("%q is an invalid OS component of %q: OSAndVersion specifier component must match %q: %w", part, specifier, osRe.String(), errInvalidArgument)
}
p.OS = normalizeOS(osVer[1])
p.OSVersion = osVer[2]
p.OS = normalizeOS(osOptions[1])
osVersion, err := decodeOSOption(osOptions[2])
if err != nil {
return specs.Platform{}, fmt.Errorf("%q has an invalid OS version %q: %w", specifier, osOptions[2], err)
}
p.OSVersion = osVersion
if osOptions[3] != "" {
p.OSFeatures, err = parseOSFeatures(osOptions[3][1:])
if err != nil {
return specs.Platform{}, fmt.Errorf("%q has invalid OS features: %w", specifier, err)
}
}
} else {
if !specifierRe.MatchString(part) {
return specs.Platform{}, fmt.Errorf("%q is an invalid component of %q: platform specifier component must match %q: %w", part, specifier, specifierRe.String(), errInvalidArgument)
@@ -296,6 +348,30 @@ func Parse(specifier string) (specs.Platform, error) {
return specs.Platform{}, fmt.Errorf("%q: cannot parse platform specifier: %w", specifier, errInvalidArgument)
}
func parseOSFeatures(s string) ([]string, error) {
if s == "" {
return nil, nil
}
var features []string
for raw := range strings.SplitSeq(s, "+") {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil, fmt.Errorf("empty os feature: %w", errInvalidArgument)
}
feature, err := decodeOSOption(raw)
if err != nil {
return nil, fmt.Errorf("invalid os feature %q: %w", raw, err)
}
if feature == "" {
continue
}
features = append(features, feature)
}
return features, nil
}
// MustParse is like Parses but panics if the specifier cannot be parsed.
// Simplifies initialization of global variables.
func MustParse(specifier string) specs.Platform {
@@ -321,12 +397,77 @@ func FormatAll(platform specs.Platform) string {
if platform.OS == "" {
return "unknown"
}
if platform.OSVersion != "" {
OSAndVersion := fmt.Sprintf(osAndVersionFormat, platform.OS, platform.OSVersion)
return path.Join(OSAndVersion, platform.Architecture, platform.Variant)
}
if platform.OSVersion == "" && len(platform.OSFeatures) == 0 {
return path.Join(platform.OS, platform.Architecture, platform.Variant)
}
var b strings.Builder
b.WriteString(platform.OS)
osv := encodeOSOption(platform.OSVersion)
formatted := formatOSFeatures(platform.OSFeatures)
if osv != "" || formatted != "" {
b.Grow(len(osv) + len(formatted) + 3) // parens + maybe '+'
b.WriteByte('(')
if osv != "" {
b.WriteString(osv)
}
if formatted != "" {
b.WriteByte('+')
b.WriteString(formatted)
}
b.WriteByte(')')
}
return path.Join(b.String(), platform.Architecture, platform.Variant)
}
func formatOSFeatures(features []string) string {
if len(features) == 0 {
return ""
}
if !slices.IsSorted(features) {
features = slices.Clone(features)
slices.Sort(features)
}
var b strings.Builder
var wrote bool
var prev string
for _, f := range features {
if f == "" || f == prev {
// skip empty and duplicate values
continue
}
prev = f
if wrote {
b.WriteByte('+')
}
b.WriteString(encodeOSOption(f))
wrote = true
}
return b.String()
}
// osOptionReplacer encodes characters in OS option values (version and
// features) that are ambiguous with the format syntax. The percent sign
// must be replaced first to avoid double-encoding.
var osOptionReplacer = strings.NewReplacer(
"%", "%25",
"+", "%2B",
"(", "%28",
")", "%29",
"/", "%2F",
)
func encodeOSOption(v string) string {
return osOptionReplacer.Replace(v)
}
func decodeOSOption(v string) (string, error) {
if strings.Contains(v, "%") {
return url.PathUnescape(v)
}
return v, nil
}
// Normalize validates and translate the platform to the canonical value.
@@ -336,6 +477,11 @@ func FormatAll(platform specs.Platform) string {
func Normalize(platform specs.Platform) specs.Platform {
platform.OS = normalizeOS(platform.OS)
platform.Architecture, platform.Variant = normalizeArch(platform.Architecture, platform.Variant)
if len(platform.OSFeatures) > 0 {
platform.OSFeatures = slices.Clone(platform.OSFeatures)
slices.Sort(platform.OSFeatures)
platform.OSFeatures = slices.Compact(platform.OSFeatures)
}
return platform
}
@@ -1,6 +1,6 @@
load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library")
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
load("@rules_proto//proto:defs.bzl", "proto_library")
package(default_visibility = ["//visibility:public"])
+17 -1
View File
@@ -71,6 +71,7 @@ type ServeMux struct {
streamErrorHandler StreamErrorHandlerFunc
routingErrorHandler RoutingErrorHandlerFunc
disablePathLengthFallback bool
disableHTTPMethodOverride bool
unescapingMode UnescapingMode
writeContentLength bool
disableChunkedEncoding bool
@@ -271,6 +272,19 @@ func WithDisablePathLengthFallback() ServeMuxOption {
}
}
// WithDisableHTTPMethodOverride returns a ServeMuxOption that disables the
// X-HTTP-Method-Override header handling.
//
// When this option is used, the mux will no longer allow POST requests with
// the X-HTTP-Method-Override header to override the HTTP method. The path
// length fallback (POST with application/x-www-form-urlencoded falling back
// to a matching GET handler) is not affected by this option.
func WithDisableHTTPMethodOverride() ServeMuxOption {
return func(serveMux *ServeMux) {
serveMux.disableHTTPMethodOverride = true
}
}
// WithWriteContentLength returns a ServeMuxOption to enable writing content length on non-streaming responses
func WithWriteContentLength() ServeMuxOption {
return func(serveMux *ServeMux) {
@@ -405,7 +419,7 @@ func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path = r.URL.RawPath
}
if override := r.Header.Get("X-HTTP-Method-Override"); override != "" && s.isPathLengthFallback(r) {
if override := r.Header.Get("X-HTTP-Method-Override"); override != "" && !s.disableHTTPMethodOverride && s.isPathLengthFallback(r) {
if err := r.ParseForm(); err != nil {
_, outboundMarshaler := MarshalerForRequest(s, r)
sterr := status.Error(codes.InvalidArgument, err.Error())
@@ -467,6 +481,7 @@ func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
HTTPStatus: http.StatusBadRequest,
Err: mse,
})
return
}
continue
}
@@ -509,6 +524,7 @@ func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
HTTPStatus: http.StatusBadRequest,
Err: mse,
})
return
}
continue
}
+24
View File
@@ -1,5 +1,11 @@
package client
import (
"strings"
"github.com/pkg/errors"
)
const (
ExporterImage = "image"
ExporterLocal = "local"
@@ -7,3 +13,21 @@ const (
ExporterOCI = "oci"
ExporterDocker = "docker"
)
type LocalExporterMode string
const (
LocalExporterModeCopy LocalExporterMode = "copy"
LocalExporterModeDelete LocalExporterMode = "delete"
)
func ParseLocalExporterMode(v string) (LocalExporterMode, error) {
switch strings.ToLower(strings.TrimSpace(v)) {
case "", string(LocalExporterModeCopy):
return LocalExporterModeCopy, nil
case string(LocalExporterModeDelete):
return LocalExporterModeDelete, nil
default:
return "", errors.Errorf("invalid local exporter mode %q", v)
}
}
+15
View File
@@ -189,8 +189,23 @@ func (c *Client) solve(ctx context.Context, def *llb.Definition, runGateway runG
if ex.OutputDir == "" {
return nil, errors.Errorf("output directory is required for %s exporter", ex.Type)
}
if ex.Type == ExporterLocal {
mode := LocalExporterModeCopy
if ex.Attrs != nil {
mode, err = ParseLocalExporterMode(ex.Attrs["mode"])
if err != nil {
return nil, err
}
}
if mode == LocalExporterModeDelete {
syncTargets = append(syncTargets, filesync.WithFSSyncDirDelete(exID, ex.OutputDir))
} else {
syncTargets = append(syncTargets, filesync.WithFSSyncDir(exID, ex.OutputDir))
}
} else {
syncTargets = append(syncTargets, filesync.WithFSSyncDir(exID, ex.OutputDir))
}
}
if supportStore {
store := ex.OutputStore
if store == nil {
+20 -4
View File
@@ -8,7 +8,6 @@ import (
"time"
"github.com/moby/buildkit/util/bklog"
"github.com/pkg/errors"
"github.com/tonistiigi/fsutil"
fstypes "github.com/tonistiigi/fsutil/types"
@@ -111,11 +110,12 @@ func recvDiffCopy(ds grpc.ClientStream, dest string, cu CacheUpdater, progress p
}))
}
func syncTargetDiffCopy(ds grpc.ServerStream, dest string) error {
func syncTargetDiffCopy(ds grpc.ServerStream, dest string, deleteMode bool) error {
if err := os.MkdirAll(dest, 0700); err != nil {
return errors.Wrapf(err, "failed to create synctarget dest dir %s", dest)
}
return errors.WithStack(fsutil.Receive(ds.Context(), ds, dest, fsutil.ReceiveOpt{
opt := fsutil.ReceiveOpt{
Merge: true,
Filter: func() func(string, *fstypes.Stat) bool {
uid := os.Getuid()
@@ -126,7 +126,23 @@ func syncTargetDiffCopy(ds grpc.ServerStream, dest string) error {
return true
}
}(),
}))
}
osRoot, err := os.OpenRoot(dest)
if err != nil {
return errors.Wrapf(err, "failed to open synctarget dest root %s", dest)
}
root := fsutil.NewRoot(osRoot)
defer root.Close()
if deleteMode {
opt.Merge = false
// Request every source file so delete mode mirrors file contents without
// relying on fsutil's path-based content comparison.
opt.Differ = fsutil.DiffNone
}
return errors.WithStack(fsutil.ReceiveRoot(ds.Context(), ds, root, opt))
}
func writeTargetFile(ds grpc.ServerStream, wc io.WriteCloser) error {
+19 -5
View File
@@ -255,6 +255,7 @@ type FSSyncTarget interface {
type fsSyncTarget struct {
id int
outdir string
deleteMode bool
f FileOutputFunc
}
@@ -276,10 +277,23 @@ func WithFSSyncDir(id int, outdir string) FSSyncTarget {
}
}
func WithFSSyncDirDelete(id int, outdir string) FSSyncTarget {
return &fsSyncTarget{
id: id,
outdir: outdir,
deleteMode: true,
}
}
type fsSyncDirTarget struct {
outdir string
deleteMode bool
}
func NewFSSyncTarget(targets ...FSSyncTarget) *SyncTarget {
st := &SyncTarget{
fs: make(map[int]FileOutputFunc),
outdirs: make(map[int]string),
outdirs: make(map[int]fsSyncDirTarget),
}
st.Add(targets...)
return st
@@ -287,7 +301,7 @@ func NewFSSyncTarget(targets ...FSSyncTarget) *SyncTarget {
type SyncTarget struct {
fs map[int]FileOutputFunc
outdirs map[int]string
outdirs map[int]fsSyncDirTarget
}
var _ session.Attachable = &SyncTarget{}
@@ -299,7 +313,7 @@ func (sp *SyncTarget) Add(targets ...FSSyncTarget) {
sp.fs[t.id] = t.f
}
if t.outdir != "" {
sp.outdirs[t.id] = t.outdir
sp.outdirs[t.id] = fsSyncDirTarget{outdir: t.outdir, deleteMode: t.deleteMode}
}
}
}
@@ -326,8 +340,8 @@ func (sp *SyncTarget) chooser(ctx context.Context) int {
func (sp *SyncTarget) DiffCopy(stream FileSend_DiffCopyServer) (err error) {
id := sp.chooser(stream.Context())
if outdir, ok := sp.outdirs[id]; ok {
return syncTargetDiffCopy(stream, outdir)
if target, ok := sp.outdirs[id]; ok {
return syncTargetDiffCopy(stream, target.outdir, target.deleteMode)
}
f, ok := sp.fs[id]
if !ok {
+2 -6
View File
@@ -30,7 +30,6 @@ const (
NetMode_UNSET NetMode = 0 // sandbox
NetMode_HOST NetMode = 1
NetMode_NONE NetMode = 2
NetMode_PROXY NetMode = 3
)
// Enum value maps for NetMode.
@@ -39,13 +38,11 @@ var (
0: "UNSET",
1: "HOST",
2: "NONE",
3: "PROXY",
}
NetMode_value = map[string]int32{
"UNSET": 0,
"HOST": 1,
"NONE": 2,
"PROXY": 3,
}
)
@@ -3845,12 +3842,11 @@ const file_github_com_moby_buildkit_solver_pb_ops_proto_rawDesc = "" +
"\x05upper\x18\x02 \x01(\v2\x12.pb.UpperDiffInputR\x05upper\"9\n" +
"\rPassthroughOp\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x18\n" +
"\aoutputs\x18\x02 \x03(\x03R\aoutputs*3\n" +
"\aoutputs\x18\x02 \x03(\x03R\aoutputs*(\n" +
"\aNetMode\x12\t\n" +
"\x05UNSET\x10\x00\x12\b\n" +
"\x04HOST\x10\x01\x12\b\n" +
"\x04NONE\x10\x02\x12\t\n" +
"\x05PROXY\x10\x03*)\n" +
"\x04NONE\x10\x02*)\n" +
"\fSecurityMode\x12\v\n" +
"\aSANDBOX\x10\x00\x12\f\n" +
"\bINSECURE\x10\x01*@\n" +
-1
View File
@@ -83,7 +83,6 @@ enum NetMode {
UNSET = 0; // sandbox
HOST = 1;
NONE = 2;
PROXY = 3;
}
enum SecurityMode {
+1
View File
@@ -5,3 +5,4 @@ cmd/tomljson/tomljson
cmd/tomltestgen/tomltestgen
dist
tests/
test-results
+33 -41
View File
@@ -1,84 +1,76 @@
[service]
golangci-lint-version = "1.39.0"
[linters-settings.wsl]
allow-assign-and-anything = true
[linters-settings.exhaustive]
default-signifies-exhaustive = true
version = "2"
[linters]
disable-all = true
default = "none"
enable = [
"asciicheck",
"bodyclose",
"cyclop",
"deadcode",
"depguard",
"dogsled",
"dupl",
"durationcheck",
"errcheck",
"errorlint",
"exhaustive",
# "exhaustivestruct",
"exportloopref",
"forbidigo",
# "forcetypeassert",
"funlen",
"gci",
# "gochecknoglobals",
"gochecknoinits",
"gocognit",
"goconst",
"gocritic",
"gocyclo",
"godot",
"godox",
# "goerr113",
"gofmt",
"gofumpt",
"godoclint",
"goheader",
"goimports",
"golint",
"gomnd",
# "gomoddirectives",
"gomodguard",
"goprintffuncname",
"gosec",
"gosimple",
"govet",
# "ifshort",
"importas",
"ineffassign",
"lll",
"makezero",
"mirror",
"misspell",
"nakedret",
"nestif",
"nilerr",
# "nlreturn",
"noctx",
"nolintlint",
#"paralleltest",
"perfsprint",
"prealloc",
"predeclared",
"revive",
"rowserrcheck",
"sqlclosecheck",
"staticcheck",
"structcheck",
"stylecheck",
# "testpackage",
"thelper",
"tparallel",
"typecheck",
"unconvert",
"unparam",
"unused",
"varcheck",
"usetesting",
"wastedassign",
"whitespace",
# "wrapcheck",
# "wsl"
]
[linters.settings.exhaustive]
default-signifies-exhaustive = true
[linters.settings.lll]
line-length = 150
[[linters.exclusions.rules]]
path = ".test.go"
linters = ["goconst", "gosec"]
[[linters.exclusions.rules]]
path = "main.go"
linters = ["forbidigo"]
[[linters.exclusions.rules]]
path = "internal"
linters = ["revive"]
text = "(exported|indent-error-flow): "
[formatters]
enable = [
"gci",
"gofmt",
"gofumpt",
"goimports",
]
-3
View File
@@ -22,7 +22,6 @@ builds:
- linux_riscv64
- windows_amd64
- windows_arm64
- windows_arm
- darwin_amd64
- darwin_arm64
- id: tomljson
@@ -42,7 +41,6 @@ builds:
- linux_riscv64
- windows_amd64
- windows_arm64
- windows_arm
- darwin_amd64
- darwin_arm64
- id: jsontoml
@@ -62,7 +60,6 @@ builds:
- linux_arm
- windows_amd64
- windows_arm64
- windows_arm
- darwin_amd64
- darwin_arm64
universal_binaries:
+64
View File
@@ -0,0 +1,64 @@
# Agent Guidelines for go-toml
This file provides guidelines for AI agents contributing to go-toml. All agents must follow these rules derived from [CONTRIBUTING.md](./CONTRIBUTING.md).
## Project Overview
go-toml is a TOML library for Go. The goal is to provide an easy-to-use and efficient TOML implementation that gets the job done without getting in the way.
## Code Change Rules
### Backward Compatibility
- **No backward-incompatible changes** unless explicitly discussed and approved
- Avoid breaking people's programs unless absolutely necessary
### Testing Requirements
- **All bug fixes must include regression tests**
- **All new code must be tested**
- Run tests before submitting: `go test -race ./...`
- Test coverage must not decrease. Check with:
```bash
go test -covermode=atomic -coverprofile=coverage.out
go tool cover -func=coverage.out
```
- All lines of code touched by changes should be covered by tests
### Performance Requirements
- go-toml aims to stay efficient; avoid performance regressions
- Run benchmarks to verify: `go test ./... -bench=. -count=10`
- Compare results using [benchstat](https://pkg.go.dev/golang.org/x/perf/cmd/benchstat)
### Documentation
- New features or feature extensions must include documentation
- Documentation lives in [README.md](./README.md) and throughout source code
### Code Style
- Follow existing code format and structure
- Code must pass `go fmt`
- Code must pass linting with the same golangci-lint version as CI (see version in `.github/workflows/lint.yml`):
```bash
# Install specific version (check lint.yml for current version)
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin <version>
# Run linter
golangci-lint run ./...
```
### Commit Messages
- Commit messages must explain **why** the change is needed
- Keep messages clear and informative even if details are in the PR description
## Pull Request Checklist
Before submitting:
1. Tests pass (`go test -race ./...`)
2. No backward-incompatible changes (unless discussed)
3. Relevant documentation added/updated
4. No performance regression (verify with benchmarks)
5. Title is clear and understandable for changelog
+51 -9
View File
@@ -33,7 +33,7 @@ The documentation is present in the [README][readme] and thorough the source
code. On release, it gets updated on [pkg.go.dev][pkg.go.dev]. To make a change
to the documentation, create a pull request with your proposed changes. For
simple changes like that, the easiest way to go is probably the "Fork this
project and edit the file" button on Github, displayed at the top right of the
project and edit the file" button on GitHub, displayed at the top right of the
file. Unless it's a trivial change (for example a typo), provide a little bit of
context in your pull request description or commit message.
@@ -92,6 +92,48 @@ However, given GitHub's new policy to _not_ run Actions on pull requests until a
maintainer clicks on button, it is highly recommended that you run them locally
as you make changes.
### Test across Go versions
The repository includes tooling to test go-toml across multiple Go versions
(1.11 through 1.25) both locally and in GitHub Actions.
#### Local testing with Docker
Prerequisites: Docker installed and running, Bash shell, `rsync` command.
```bash
# Test all Go versions in parallel (default)
./test-go-versions.sh
# Test specific versions
./test-go-versions.sh 1.21 1.22 1.23
# Test sequentially (slower but uses less resources)
./test-go-versions.sh --sequential
# Verbose output with custom results directory
./test-go-versions.sh --verbose --output ./my-results 1.24 1.25
# Show all options
./test-go-versions.sh --help
```
The script creates Docker containers for each Go version and runs the full test
suite. Results are saved to a `test-results/` directory with individual logs and
a comprehensive summary report.
The script only exits with a non-zero status code if either of the two most
recent Go versions fail.
#### GitHub Actions testing (maintainers)
1. Go to the **Actions** tab in the GitHub repository
2. Select **"Go Versions Compatibility Test"** from the workflow list
3. Click **"Run workflow"**
4. Optionally customize:
- **Go versions**: Space-separated list (e.g., `1.21 1.22 1.23`)
- **Execution mode**: Parallel (faster) or sequential (more stable)
### Check coverage
We use `go tool cover` to compute test coverage. Most code editors have a way to
@@ -111,7 +153,7 @@ code lowers the coverage.
Go-toml aims to stay efficient. We rely on a set of scenarios executed with Go's
builtin benchmark systems. Because of their noisy nature, containers provided by
Github Actions cannot be reliably used for benchmarking. As a result, you are
GitHub Actions cannot be reliably used for benchmarking. As a result, you are
responsible for checking that your changes do not incur a performance penalty.
You can run their following to execute benchmarks:
@@ -168,13 +210,13 @@ Checklist:
1. Decide on the next version number. Use semver. Review commits since last
version to assess.
2. Tag release. For example:
```
git checkout v2
git pull
git tag v2.2.0
git push --tags
```
3. CI automatically builds a draft Github release. Review it and edit as
```
git checkout v2
git pull
git tag v2.2.0
git push --tags
```
3. CI automatically builds a draft GitHub release. Review it and edit as
necessary. Look for "Other changes". That would indicate a pull request not
labeled properly. Tweak labels and pull request titles until changelog looks
good for users.
+78 -34
View File
@@ -21,8 +21,6 @@ documentation.
import "github.com/pelletier/go-toml/v2"
```
See [Modules](#Modules).
## Features
### Stdlib behavior
@@ -107,7 +105,11 @@ type MyConfig struct {
### Unmarshaling
[`Unmarshal`][unmarshal] reads a TOML document and fills a Go structure with its
content. For example:
content.
Note that the struct variable names are _capitalized_, while the variables in the toml document are _lowercase_.
For example:
```go
doc := `
@@ -133,6 +135,62 @@ fmt.Println("tags:", cfg.Tags)
[unmarshal]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Unmarshal
Here is an example using tables with some simple nesting:
```go
doc := `
age = 45
fruits = ["apple", "pear"]
# these are very important!
[my-variables]
first = 1
second = 0.2
third = "abc"
# this is not so important.
[my-variables.b]
bfirst = 123
`
var Document struct {
Age int
Fruits []string
Myvariables struct {
First int
Second float64
Third string
B struct {
Bfirst int
}
} `toml:"my-variables"`
}
err := toml.Unmarshal([]byte(doc), &Document)
if err != nil {
panic(err)
}
fmt.Println("age:", Document.Age)
fmt.Println("fruits:", Document.Fruits)
fmt.Println("my-variables.first:", Document.Myvariables.First)
fmt.Println("my-variables.second:", Document.Myvariables.Second)
fmt.Println("my-variables.third:", Document.Myvariables.Third)
fmt.Println("my-variables.B.Bfirst:", Document.Myvariables.B.Bfirst)
// Output:
// age: 45
// fruits: [apple pear]
// my-variables.first: 1
// my-variables.second: 0.2
// my-variables.third: abc
// my-variables.B.Bfirst: 123
```
### Marshaling
[`Marshal`][marshal] is the opposite of Unmarshal: it represents a Go structure
@@ -179,12 +237,12 @@ Execution time speedup compared to other Go TOML libraries:
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead>
<tbody>
<tr><td>Marshal/HugoFrontMatter-2</td><td>1.9x</td><td>2.2x</td></tr>
<tr><td>Marshal/ReferenceFile/map-2</td><td>1.7x</td><td>2.1x</td></tr>
<tr><td>Marshal/ReferenceFile/struct-2</td><td>2.2x</td><td>3.0x</td></tr>
<tr><td>Unmarshal/HugoFrontMatter-2</td><td>2.9x</td><td>2.7x</td></tr>
<tr><td>Unmarshal/ReferenceFile/map-2</td><td>2.6x</td><td>2.7x</td></tr>
<tr><td>Unmarshal/ReferenceFile/struct-2</td><td>4.6x</td><td>5.1x</td></tr>
<tr><td>Marshal/HugoFrontMatter-2</td><td>2.1x</td><td>2.0x</td></tr>
<tr><td>Marshal/ReferenceFile/map-2</td><td>2.0x</td><td>2.0x</td></tr>
<tr><td>Marshal/ReferenceFile/struct-2</td><td>2.3x</td><td>2.5x</td></tr>
<tr><td>Unmarshal/HugoFrontMatter-2</td><td>3.3x</td><td>2.8x</td></tr>
<tr><td>Unmarshal/ReferenceFile/map-2</td><td>2.9x</td><td>3.0x</td></tr>
<tr><td>Unmarshal/ReferenceFile/struct-2</td><td>4.8x</td><td>5.0x</td></tr>
</tbody>
</table>
<details><summary>See more</summary>
@@ -197,36 +255,22 @@ provided for completeness.</p>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead>
<tbody>
<tr><td>Marshal/SimpleDocument/map-2</td><td>1.8x</td><td>2.7x</td></tr>
<tr><td>Marshal/SimpleDocument/struct-2</td><td>2.7x</td><td>3.8x</td></tr>
<tr><td>Unmarshal/SimpleDocument/map-2</td><td>3.8x</td><td>3.0x</td></tr>
<tr><td>Unmarshal/SimpleDocument/struct-2</td><td>5.6x</td><td>4.1x</td></tr>
<tr><td>UnmarshalDataset/example-2</td><td>3.0x</td><td>3.2x</td></tr>
<tr><td>UnmarshalDataset/code-2</td><td>2.3x</td><td>2.9x</td></tr>
<tr><td>UnmarshalDataset/twitter-2</td><td>2.6x</td><td>2.7x</td></tr>
<tr><td>UnmarshalDataset/citm_catalog-2</td><td>2.2x</td><td>2.3x</td></tr>
<tr><td>UnmarshalDataset/canada-2</td><td>1.8x</td><td>1.5x</td></tr>
<tr><td>UnmarshalDataset/config-2</td><td>4.1x</td><td>2.9x</td></tr>
<tr><td>geomean</td><td>2.7x</td><td>2.8x</td></tr>
<tr><td>Marshal/SimpleDocument/map-2</td><td>2.0x</td><td>2.9x</td></tr>
<tr><td>Marshal/SimpleDocument/struct-2</td><td>2.5x</td><td>3.6x</td></tr>
<tr><td>Unmarshal/SimpleDocument/map-2</td><td>4.2x</td><td>3.4x</td></tr>
<tr><td>Unmarshal/SimpleDocument/struct-2</td><td>5.9x</td><td>4.4x</td></tr>
<tr><td>UnmarshalDataset/example-2</td><td>3.2x</td><td>2.9x</td></tr>
<tr><td>UnmarshalDataset/code-2</td><td>2.4x</td><td>2.8x</td></tr>
<tr><td>UnmarshalDataset/twitter-2</td><td>2.7x</td><td>2.5x</td></tr>
<tr><td>UnmarshalDataset/citm_catalog-2</td><td>2.3x</td><td>2.3x</td></tr>
<tr><td>UnmarshalDataset/canada-2</td><td>1.9x</td><td>1.5x</td></tr>
<tr><td>UnmarshalDataset/config-2</td><td>5.4x</td><td>3.0x</td></tr>
<tr><td>geomean</td><td>2.9x</td><td>2.8x</td></tr>
</tbody>
</table>
<p>This table can be generated with <code>./ci.sh benchmark -a -html</code>.</p>
</details>
## Modules
go-toml uses Go's standard modules system.
Installation instructions:
- Go ≥ 1.16: Nothing to do. Use the import in your code. The `go` command deals
with it automatically.
- Go ≥ 1.13: `GO111MODULE=on go get github.com/pelletier/go-toml/v2`.
In case of trouble: [Go Modules FAQ][mod-faq].
[mod-faq]: https://github.com/golang/go/wiki/Modules#why-does-installing-a-tool-via-go-get-fail-with-error-cannot-find-main-module
## Tools
Go-toml provides three handy command line tools:
+6 -1
View File
@@ -147,7 +147,7 @@ bench() {
pushd "$dir"
if [ "${replace}" != "" ]; then
find ./benchmark/ -iname '*.go' -exec sed -i -E "s|github.com/pelletier/go-toml/v2|${replace}|g" {} \;
find ./benchmark/ -iname '*.go' -exec sed -i -E "s|github.com/pelletier/go-toml/v2\"|${replace}\"|g" {} \;
go get "${replace}"
fi
@@ -195,6 +195,11 @@ for line in reversed(lines[2:]):
"%.1fx" % (float(line[3])/v2), # v1
"%.1fx" % (float(line[7])/v2), # bs
])
if not results:
print("No benchmark results to display.", file=sys.stderr)
sys.exit(1)
# move geomean to the end
results.append(results[0])
del results[0]
+2 -3
View File
@@ -230,8 +230,8 @@ func parseLocalTime(b []byte) (LocalTime, []byte, error) {
return t, nil, err
}
if t.Second > 60 {
return t, nil, unstable.NewParserError(b[6:8], "seconds cannot be greater 60")
if t.Second > 59 {
return t, nil, unstable.NewParserError(b[6:8], "seconds cannot be greater than 59")
}
b = b[8:]
@@ -279,7 +279,6 @@ func parseLocalTime(b []byte) (LocalTime, []byte, error) {
return t, b, nil
}
//nolint:cyclop
func parseFloat(b []byte) (float64, error) {
if len(b) == 4 && (b[0] == '+' || b[0] == '-') && b[1] == 'n' && b[2] == 'a' && b[3] == 'n' {
return math.NaN(), nil
+35 -4
View File
@@ -2,10 +2,10 @@ package toml
import (
"fmt"
"reflect"
"strconv"
"strings"
"github.com/pelletier/go-toml/v2/internal/danger"
"github.com/pelletier/go-toml/v2/unstable"
)
@@ -54,6 +54,18 @@ func (s *StrictMissingError) String() string {
return buf.String()
}
// Unwrap returns wrapped decode errors
//
// Implements errors.Join() interface.
func (s *StrictMissingError) Unwrap() []error {
errs := make([]error, len(s.Errors))
for i := range s.Errors {
errs[i] = &s.Errors[i]
}
return errs
}
// Key represents a TOML key as a sequence of key parts.
type Key []string
// Error returns the error message contained in the DecodeError.
@@ -78,7 +90,7 @@ func (e *DecodeError) Key() Key {
return e.key
}
// decodeErrorFromHighlight creates a DecodeError referencing a highlighted
// wrapDecodeError creates a DecodeError referencing a highlighted
// range of bytes from document.
//
// highlight needs to be a sub-slice of document, or this function panics.
@@ -88,7 +100,7 @@ func (e *DecodeError) Key() Key {
//
//nolint:funlen
func wrapDecodeError(document []byte, de *unstable.ParserError) *DecodeError {
offset := danger.SubsliceOffset(document, de.Highlight)
offset := subsliceOffset(document, de.Highlight)
errMessage := de.Error()
errLine, errColumn := positionAtEnd(document[:offset])
@@ -248,5 +260,24 @@ func positionAtEnd(b []byte) (row int, column int) {
}
}
return
return row, column
}
// subsliceOffset returns the byte offset of subslice within data.
// subslice must share the same backing array as data.
func subsliceOffset(data []byte, subslice []byte) int {
if len(subslice) == 0 {
return 0
}
// Use reflect to get the data pointers of both slices.
// This is safe because we're only reading the pointer values for comparison.
dataPtr := reflect.ValueOf(data).Pointer()
subPtr := reflect.ValueOf(subslice).Pointer()
offset := int(subPtr - dataPtr)
if offset < 0 || offset > len(data) {
panic("subslice is not within data")
}
return offset
}
+3 -3
View File
@@ -1,6 +1,6 @@
package characters
var invalidAsciiTable = [256]bool{
var invalidASCIITable = [256]bool{
0x00: true,
0x01: true,
0x02: true,
@@ -37,6 +37,6 @@ var invalidAsciiTable = [256]bool{
0x7F: true,
}
func InvalidAscii(b byte) bool {
return invalidAsciiTable[b]
func InvalidASCII(b byte) bool {
return invalidASCIITable[b]
}
+22 -46
View File
@@ -1,20 +1,12 @@
// Package characters provides functions for working with string encodings.
package characters
import (
"unicode/utf8"
)
type utf8Err struct {
Index int
Size int
}
func (u utf8Err) Zero() bool {
return u.Size == 0
}
// Verified that a given string is only made of valid UTF-8 characters allowed
// by the TOML spec:
// Utf8TomlValidAlreadyEscaped verifies that a given string is only made of
// valid UTF-8 characters allowed by the TOML spec:
//
// Any Unicode character may be used except those that must be escaped:
// quotation mark, backslash, and the control characters other than tab (U+0000
@@ -23,8 +15,8 @@ func (u utf8Err) Zero() bool {
// It is a copy of the Go 1.17 utf8.Valid implementation, tweaked to exit early
// when a character is not allowed.
//
// The returned utf8Err is Zero() if the string is valid, or contains the byte
// index and size of the invalid character.
// The returned slice is empty if the string is valid, or contains the bytes
// of the invalid character.
//
// quotation mark => already checked
// backslash => already checked
@@ -32,9 +24,8 @@ func (u utf8Err) Zero() bool {
// 0x9 => tab, ok
// 0xA - 0x1F => invalid
// 0x7F => invalid
func Utf8TomlValidAlreadyEscaped(p []byte) (err utf8Err) {
func Utf8TomlValidAlreadyEscaped(p []byte) []byte {
// Fast path. Check for and skip 8 bytes of ASCII characters per iteration.
offset := 0
for len(p) >= 8 {
// Combining two 32 bit loads allows the same code to be used
// for 32 and 64 bit platforms.
@@ -48,24 +39,19 @@ func Utf8TomlValidAlreadyEscaped(p []byte) (err utf8Err) {
}
for i, b := range p[:8] {
if InvalidAscii(b) {
err.Index = offset + i
err.Size = 1
return
if InvalidASCII(b) {
return p[i : i+1]
}
}
p = p[8:]
offset += 8
}
n := len(p)
for i := 0; i < n; {
pi := p[i]
if pi < utf8.RuneSelf {
if InvalidAscii(pi) {
err.Index = offset + i
err.Size = 1
return
if InvalidASCII(pi) {
return p[i : i+1]
}
i++
continue
@@ -73,44 +59,34 @@ func Utf8TomlValidAlreadyEscaped(p []byte) (err utf8Err) {
x := first[pi]
if x == xx {
// Illegal starter byte.
err.Index = offset + i
err.Size = 1
return
return p[i : i+1]
}
size := int(x & 7)
if i+size > n {
// Short or invalid.
err.Index = offset + i
err.Size = n - i
return
return p[i:n]
}
accept := acceptRanges[x>>4]
if c := p[i+1]; c < accept.lo || accept.hi < c {
err.Index = offset + i
err.Size = 2
return
} else if size == 2 {
return p[i : i+2]
} else if size == 2 { //revive:disable:empty-block
} else if c := p[i+2]; c < locb || hicb < c {
err.Index = offset + i
err.Size = 3
return
} else if size == 3 {
return p[i : i+3]
} else if size == 3 { //revive:disable:empty-block
} else if c := p[i+3]; c < locb || hicb < c {
err.Index = offset + i
err.Size = 4
return
return p[i : i+4]
}
i += size
}
return
return nil
}
// Return the size of the next rune if valid, 0 otherwise.
// Utf8ValidNext returns the size of the next rune if valid, 0 otherwise.
func Utf8ValidNext(p []byte) int {
c := p[0]
if c < utf8.RuneSelf {
if InvalidAscii(c) {
if InvalidASCII(c) {
return 0
}
return 1
@@ -129,10 +105,10 @@ func Utf8ValidNext(p []byte) int {
accept := acceptRanges[x>>4]
if c := p[1]; c < accept.lo || accept.hi < c {
return 0
} else if size == 2 {
} else if size == 2 { //nolint:revive
} else if c := p[2]; c < locb || hicb < c {
return 0
} else if size == 3 {
} else if size == 3 { //nolint:revive
} else if c := p[3]; c < locb || hicb < c {
return 0
}
-65
View File
@@ -1,65 +0,0 @@
package danger
import (
"fmt"
"reflect"
"unsafe"
)
const maxInt = uintptr(int(^uint(0) >> 1))
func SubsliceOffset(data []byte, subslice []byte) int {
datap := (*reflect.SliceHeader)(unsafe.Pointer(&data))
hlp := (*reflect.SliceHeader)(unsafe.Pointer(&subslice))
if hlp.Data < datap.Data {
panic(fmt.Errorf("subslice address (%d) is before data address (%d)", hlp.Data, datap.Data))
}
offset := hlp.Data - datap.Data
if offset > maxInt {
panic(fmt.Errorf("slice offset larger than int (%d)", offset))
}
intoffset := int(offset)
if intoffset > datap.Len {
panic(fmt.Errorf("slice offset (%d) is farther than data length (%d)", intoffset, datap.Len))
}
if intoffset+hlp.Len > datap.Len {
panic(fmt.Errorf("slice ends (%d+%d) is farther than data length (%d)", intoffset, hlp.Len, datap.Len))
}
return intoffset
}
func BytesRange(start []byte, end []byte) []byte {
if start == nil || end == nil {
panic("cannot call BytesRange with nil")
}
startp := (*reflect.SliceHeader)(unsafe.Pointer(&start))
endp := (*reflect.SliceHeader)(unsafe.Pointer(&end))
if startp.Data > endp.Data {
panic(fmt.Errorf("start pointer address (%d) is after end pointer address (%d)", startp.Data, endp.Data))
}
l := startp.Len
endLen := int(endp.Data-startp.Data) + endp.Len
if endLen > l {
l = endLen
}
if l > startp.Cap {
panic(fmt.Errorf("range length is larger than capacity"))
}
return start[:l]
}
func Stride(ptr unsafe.Pointer, size uintptr, offset int) unsafe.Pointer {
// TODO: replace with unsafe.Add when Go 1.17 is released
// https://github.com/golang/go/issues/40481
return unsafe.Pointer(uintptr(ptr) + uintptr(int(size)*offset))
}
-23
View File
@@ -1,23 +0,0 @@
package danger
import (
"reflect"
"unsafe"
)
// typeID is used as key in encoder and decoder caches to enable using
// the optimize runtime.mapaccess2_fast64 function instead of the more
// expensive lookup if we were to use reflect.Type as map key.
//
// typeID holds the pointer to the reflect.Type value, which is unique
// in the program.
//
// https://github.com/segmentio/encoding/blob/master/json/codec.go#L59-L61
type TypeID unsafe.Pointer
func MakeTypeID(t reflect.Type) TypeID {
// reflect.Type has the fields:
// typ unsafe.Pointer
// ptr unsafe.Pointer
return TypeID((*[2]unsafe.Pointer)(unsafe.Pointer(&t))[1])
}
+1 -1
View File
@@ -36,7 +36,7 @@ func (t *KeyTracker) Pop(node *unstable.Node) {
}
}
// Key returns the current key
// Key returns the current key.
func (t *KeyTracker) Key() []string {
k := make([]string, len(t.k))
copy(k, t.k)
+7 -6
View File
@@ -288,11 +288,12 @@ func (s *SeenTracker) checkKeyValue(node *unstable.Node) (bool, error) {
idx = s.create(parentIdx, k, tableKind, false, true)
} else {
entry := s.entries[idx]
if it.IsLast() {
switch {
case it.IsLast():
return false, fmt.Errorf("toml: key %s is already defined", string(k))
} else if entry.kind != tableKind {
case entry.kind != tableKind:
return false, fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
} else if entry.explicit {
case entry.explicit:
return false, fmt.Errorf("toml: cannot redefine table %s that has already been explicitly defined", string(k))
}
}
@@ -309,16 +310,16 @@ func (s *SeenTracker) checkKeyValue(node *unstable.Node) (bool, error) {
return s.checkInlineTable(value)
case unstable.Array:
return s.checkArray(value)
}
default:
return false, nil
}
}
func (s *SeenTracker) checkArray(node *unstable.Node) (first bool, err error) {
it := node.Children()
for it.Next() {
n := it.Node()
switch n.Kind {
switch n.Kind { //nolint:exhaustive
case unstable.InlineTable:
first, err = s.checkInlineTable(n)
if err != nil {
+1
View File
@@ -1 +1,2 @@
// Package tracker provides functions for keeping track of AST nodes.
package tracker
+1 -1
View File
@@ -45,7 +45,7 @@ func (d *LocalDate) UnmarshalText(b []byte) error {
type LocalTime struct {
Hour int // Hour of the day: [0; 24[
Minute int // Minute of the hour: [0; 60[
Second int // Second of the minute: [0; 60[
Second int // Second of the minute: [0; 59]
Nanosecond int // Nanoseconds within the second: [0, 1000000000[
Precision int // Number of digits to display for Nanosecond.
}
+115 -46
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"encoding"
"encoding/json"
"errors"
"fmt"
"io"
"math"
@@ -42,7 +43,7 @@ type Encoder struct {
arraysMultiline bool
indentSymbol string
indentTables bool
marshalJsonNumbers bool
marshalJSONNumbers bool
}
// NewEncoder returns a new Encoder that writes to w.
@@ -89,14 +90,14 @@ func (enc *Encoder) SetIndentTables(indent bool) *Encoder {
return enc
}
// SetMarshalJsonNumbers forces the encoder to serialize `json.Number` as a
// SetMarshalJSONNumbers forces the encoder to serialize `json.Number` as a
// float or integer instead of relying on TextMarshaler to emit a string.
//
// *Unstable:* This method does not follow the compatibility guarantees of
// semver. It can be changed or removed without a new major version being
// issued.
func (enc *Encoder) SetMarshalJsonNumbers(indent bool) *Encoder {
enc.marshalJsonNumbers = indent
func (enc *Encoder) SetMarshalJSONNumbers(indent bool) *Encoder {
enc.marshalJSONNumbers = indent
return enc
}
@@ -161,6 +162,8 @@ func (enc *Encoder) SetMarshalJsonNumbers(indent bool) *Encoder {
//
// The "omitempty" option prevents empty values or groups from being emitted.
//
// The "omitzero" option prevents zero values or groups from being emitted.
//
// The "commented" option prefixes the value and all its children with a comment
// symbol.
//
@@ -177,7 +180,7 @@ func (enc *Encoder) Encode(v interface{}) error {
ctx.inline = enc.tablesInline
if v == nil {
return fmt.Errorf("toml: cannot encode a nil interface")
return errors.New("toml: cannot encode a nil interface")
}
b, err := enc.encode(b, ctx, reflect.ValueOf(v))
@@ -196,6 +199,7 @@ func (enc *Encoder) Encode(v interface{}) error {
type valueOptions struct {
multiline bool
omitempty bool
omitzero bool
commented bool
comment string
}
@@ -266,16 +270,15 @@ func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, e
case LocalDateTime:
return append(b, x.String()...), nil
case json.Number:
if enc.marshalJsonNumbers {
if enc.marshalJSONNumbers {
if x == "" { /// Useful zero value.
return append(b, "0"...), nil
} else if v, err := x.Int64(); err == nil {
return enc.encode(b, ctx, reflect.ValueOf(v))
} else if f, err := x.Float64(); err == nil {
return enc.encode(b, ctx, reflect.ValueOf(f))
} else {
return nil, fmt.Errorf("toml: unable to convert %q to int64 or float64", x)
}
return nil, fmt.Errorf("toml: unable to convert %q to int64 or float64", x)
}
}
@@ -309,7 +312,7 @@ func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, e
return enc.encodeSlice(b, ctx, v)
case reflect.Interface:
if v.IsNil() {
return nil, fmt.Errorf("toml: encoding a nil interface is not supported")
return nil, errors.New("toml: encoding a nil interface is not supported")
}
return enc.encode(b, ctx, v.Elem())
@@ -326,28 +329,30 @@ func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, e
case reflect.Float32:
f := v.Float()
if math.IsNaN(f) {
switch {
case math.IsNaN(f):
b = append(b, "nan"...)
} else if f > math.MaxFloat32 {
case f > math.MaxFloat32:
b = append(b, "inf"...)
} else if f < -math.MaxFloat32 {
case f < -math.MaxFloat32:
b = append(b, "-inf"...)
} else if math.Trunc(f) == f {
case math.Trunc(f) == f:
b = strconv.AppendFloat(b, f, 'f', 1, 32)
} else {
default:
b = strconv.AppendFloat(b, f, 'f', -1, 32)
}
case reflect.Float64:
f := v.Float()
if math.IsNaN(f) {
switch {
case math.IsNaN(f):
b = append(b, "nan"...)
} else if f > math.MaxFloat64 {
case f > math.MaxFloat64:
b = append(b, "inf"...)
} else if f < -math.MaxFloat64 {
case f < -math.MaxFloat64:
b = append(b, "-inf"...)
} else if math.Trunc(f) == f {
case math.Trunc(f) == f:
b = strconv.AppendFloat(b, f, 'f', 1, 64)
} else {
default:
b = strconv.AppendFloat(b, f, 'f', -1, 64)
}
case reflect.Bool:
@@ -384,6 +389,31 @@ func shouldOmitEmpty(options valueOptions, v reflect.Value) bool {
return options.omitempty && isEmptyValue(v)
}
func shouldOmitZero(options valueOptions, v reflect.Value) bool {
if !options.omitzero {
return false
}
// Check if the type implements isZeroer interface (has a custom IsZero method).
if v.Type().Implements(isZeroerType) {
return v.Interface().(isZeroer).IsZero()
}
// Check if pointer type implements isZeroer.
if reflect.PointerTo(v.Type()).Implements(isZeroerType) {
if v.CanAddr() {
return v.Addr().Interface().(isZeroer).IsZero()
}
// Create a temporary addressable copy to call the pointer receiver method.
pv := reflect.New(v.Type())
pv.Elem().Set(v)
return pv.Interface().(isZeroer).IsZero()
}
// Fall back to reflect's IsZero for types without custom IsZero method.
return v.IsZero()
}
func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v reflect.Value) ([]byte, error) {
var err error
@@ -434,8 +464,9 @@ func isEmptyValue(v reflect.Value) bool {
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
}
default:
return false
}
}
func isEmptyStruct(v reflect.Value) bool {
@@ -479,7 +510,7 @@ func (enc *Encoder) encodeString(b []byte, v string, options valueOptions) []byt
func needsQuoting(v string) bool {
// TODO: vectorize
for _, b := range []byte(v) {
if b == '\'' || b == '\r' || b == '\n' || characters.InvalidAscii(b) {
if b == '\'' || b == '\r' || b == '\n' || characters.InvalidASCII(b) {
return true
}
}
@@ -517,12 +548,26 @@ func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byt
del = 0x7f
)
for _, r := range []byte(v) {
bv := []byte(v)
for i := 0; i < len(bv); i++ {
r := bv[i]
switch r {
case '\\':
b = append(b, `\\`...)
case '"':
if multiline {
// Quotation marks do not need to be quoted in multiline strings unless
// it contains 3 consecutive. If 3+ quotes appear, quote all of them
// because it's visually better
if i+2 > len(bv) || bv[i+1] != '"' || bv[i+2] != '"' {
b = append(b, r)
} else {
b = append(b, `\"\"\"`...)
i += 2
}
} else {
b = append(b, `\"`...)
}
case '\b':
b = append(b, `\b`...)
case '\f':
@@ -559,9 +604,9 @@ func (enc *Encoder) encodeUnquotedKey(b []byte, v string) []byte {
return append(b, v...)
}
func (enc *Encoder) encodeTableHeader(ctx encoderCtx, b []byte) ([]byte, error) {
func (enc *Encoder) encodeTableHeader(ctx encoderCtx, b []byte) []byte {
if len(ctx.parentKey) == 0 {
return b, nil
return b
}
b = enc.encodeComment(ctx.indent, ctx.options.comment, b)
@@ -581,10 +626,9 @@ func (enc *Encoder) encodeTableHeader(ctx encoderCtx, b []byte) ([]byte, error)
b = append(b, "]\n"...)
return b, nil
return b
}
//nolint:cyclop
func (enc *Encoder) encodeKey(b []byte, k string) []byte {
needsQuotation := false
cannotUseLiteral := false
@@ -621,30 +665,33 @@ func (enc *Encoder) encodeKey(b []byte, k string) []byte {
func (enc *Encoder) keyToString(k reflect.Value) (string, error) {
keyType := k.Type()
switch {
case keyType.Kind() == reflect.String:
return k.String(), nil
case keyType.Implements(textMarshalerType):
if keyType.Implements(textMarshalerType) {
keyB, err := k.Interface().(encoding.TextMarshaler).MarshalText()
if err != nil {
return "", fmt.Errorf("toml: error marshalling key %v from text: %w", k, err)
}
return string(keyB), nil
}
case keyType.Kind() == reflect.Int || keyType.Kind() == reflect.Int8 || keyType.Kind() == reflect.Int16 || keyType.Kind() == reflect.Int32 || keyType.Kind() == reflect.Int64:
switch keyType.Kind() {
case reflect.String:
return k.String(), nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.FormatInt(k.Int(), 10), nil
case keyType.Kind() == reflect.Uint || keyType.Kind() == reflect.Uint8 || keyType.Kind() == reflect.Uint16 || keyType.Kind() == reflect.Uint32 || keyType.Kind() == reflect.Uint64:
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return strconv.FormatUint(k.Uint(), 10), nil
case keyType.Kind() == reflect.Float32:
case reflect.Float32:
return strconv.FormatFloat(k.Float(), 'f', -1, 32), nil
case keyType.Kind() == reflect.Float64:
case reflect.Float64:
return strconv.FormatFloat(k.Float(), 'f', -1, 64), nil
}
default:
return "", fmt.Errorf("toml: type %s is not supported as a map key", keyType.Kind())
}
}
func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
@@ -657,9 +704,19 @@ func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte
for iter.Next() {
v := iter.Value()
if isNil(v) {
// Handle nil values: convert nil pointers to zero value,
// skip nil interfaces and nil maps.
switch v.Kind() {
case reflect.Ptr:
if v.IsNil() {
v = reflect.Zero(v.Type().Elem())
}
case reflect.Interface, reflect.Map:
if v.IsNil() {
continue
}
default:
}
k, err := enc.keyToString(iter.Key())
if err != nil {
@@ -748,9 +805,8 @@ func walkStruct(ctx encoderCtx, t *table, v reflect.Value) {
walkStruct(ctx, t, f.Elem())
}
continue
} else {
k = fieldType.Name
}
k = fieldType.Name
}
if isNil(f) {
@@ -760,6 +816,7 @@ func walkStruct(ctx encoderCtx, t *table, v reflect.Value) {
options := valueOptions{
multiline: opts.multiline,
omitempty: opts.omitempty,
omitzero: opts.omitzero,
commented: opts.commented,
comment: fieldType.Tag.Get("comment"),
}
@@ -820,6 +877,7 @@ type tagOptions struct {
multiline bool
inline bool
omitempty bool
omitzero bool
commented bool
}
@@ -832,7 +890,7 @@ func parseTag(tag string) (string, tagOptions) {
}
raw := tag[idx+1:]
tag = string(tag[:idx])
tag = tag[:idx]
for raw != "" {
var o string
i := strings.Index(raw, ",")
@@ -848,6 +906,8 @@ func parseTag(tag string) (string, tagOptions) {
opts.inline = true
case "omitempty":
opts.omitempty = true
case "omitzero":
opts.omitzero = true
case "commented":
opts.commented = true
}
@@ -866,10 +926,7 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro
}
if !ctx.skipTableHeader {
b, err = enc.encodeTableHeader(ctx, b)
if err != nil {
return nil, err
}
b = enc.encodeTableHeader(ctx, b)
if enc.indentTables && len(ctx.parentKey) > 0 {
ctx.indent++
@@ -882,6 +939,9 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro
if shouldOmitEmpty(kv.Options, kv.Value) {
continue
}
if kv.Options.omitzero && shouldOmitZero(kv.Options, kv.Value) {
continue
}
hasNonEmptyKV = true
ctx.setKey(kv.Key)
@@ -901,6 +961,9 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro
if shouldOmitEmpty(table.Options, table.Value) {
continue
}
if table.Options.omitzero && shouldOmitZero(table.Options, table.Value) {
continue
}
if first {
first = false
if hasNonEmptyKV {
@@ -935,6 +998,9 @@ func (enc *Encoder) encodeTableInline(b []byte, ctx encoderCtx, t table) ([]byte
if shouldOmitEmpty(kv.Options, kv.Value) {
continue
}
if kv.Options.omitzero && shouldOmitZero(kv.Options, kv.Value) {
continue
}
if first {
first = false
@@ -963,11 +1029,14 @@ func willConvertToTable(ctx encoderCtx, v reflect.Value) bool {
if !v.IsValid() {
return false
}
if v.Type() == timeType || v.Type().Implements(textMarshalerType) || (v.Kind() != reflect.Ptr && v.CanAddr() && reflect.PointerTo(v.Type()).Implements(textMarshalerType)) {
t := v.Type()
if t == timeType || t.Implements(textMarshalerType) {
return false
}
if v.Kind() != reflect.Ptr && v.CanAddr() && reflect.PointerTo(t).Implements(textMarshalerType) {
return false
}
t := v.Type()
switch t.Kind() {
case reflect.Map, reflect.Struct:
return !ctx.inline
+16 -9
View File
@@ -1,7 +1,6 @@
package toml
import (
"github.com/pelletier/go-toml/v2/internal/danger"
"github.com/pelletier/go-toml/v2/internal/tracker"
"github.com/pelletier/go-toml/v2/unstable"
)
@@ -13,6 +12,9 @@ type strict struct {
key tracker.KeyTracker
missing []unstable.ParserError
// Reference to the document for computing key ranges.
doc []byte
}
func (s *strict) EnterTable(node *unstable.Node) {
@@ -53,7 +55,7 @@ func (s *strict) MissingTable(node *unstable.Node) {
}
s.missing = append(s.missing, unstable.ParserError{
Highlight: keyLocation(node),
Highlight: s.keyLocation(node),
Message: "missing table",
Key: s.key.Key(),
})
@@ -65,8 +67,8 @@ func (s *strict) MissingField(node *unstable.Node) {
}
s.missing = append(s.missing, unstable.ParserError{
Highlight: keyLocation(node),
Message: "missing field",
Highlight: s.keyLocation(node),
Message: "unknown field",
Key: s.key.Key(),
})
}
@@ -88,7 +90,7 @@ func (s *strict) Error(doc []byte) error {
return err
}
func keyLocation(node *unstable.Node) []byte {
func (s *strict) keyLocation(node *unstable.Node) []byte {
k := node.Key()
hasOne := k.Next()
@@ -96,12 +98,17 @@ func keyLocation(node *unstable.Node) []byte {
panic("should not be called with empty key")
}
start := k.Node().Data
end := k.Node().Data
// Get the range from the first key to the last key.
firstRaw := k.Node().Raw
lastRaw := firstRaw
for k.Next() {
end = k.Node().Data
lastRaw = k.Node().Raw
}
return danger.BytesRange(start, end)
// Compute the slice from the document using the ranges.
start := firstRaw.Offset
end := lastRaw.Offset + lastRaw.Length
return s.doc[start:end]
}
+597
View File
@@ -0,0 +1,597 @@
#!/usr/bin/env bash
set -uo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Go versions to test (1.11 through 1.26)
GO_VERSIONS=(
"1.11"
"1.12"
"1.13"
"1.14"
"1.15"
"1.16"
"1.17"
"1.18"
"1.19"
"1.20"
"1.21"
"1.22"
"1.23"
"1.24"
"1.25"
"1.26"
)
# Default values
PARALLEL=true
VERBOSE=false
OUTPUT_DIR="test-results"
DOCKER_TIMEOUT="10m"
usage() {
cat << EOF
Usage: $0 [OPTIONS] [GO_VERSIONS...]
Test go-toml across multiple Go versions using Docker containers.
The script reports the lowest continuous supported Go version (where all subsequent
versions pass) and only exits with non-zero status if either of the two most recent
Go versions fail, indicating immediate attention is needed.
Note: For Go versions < 1.21, the script automatically updates go.mod to match the
target version, but older versions may still fail due to missing standard library
features (e.g., the 'slices' package introduced in Go 1.21).
OPTIONS:
-h, --help Show this help message
-s, --sequential Run tests sequentially instead of in parallel
-v, --verbose Enable verbose output
-o, --output DIR Output directory for test results (default: test-results)
-t, --timeout TIME Docker timeout for each test (default: 10m)
--list List available Go versions and exit
ARGUMENTS:
GO_VERSIONS Specific Go versions to test (default: all supported versions)
Examples: 1.21 1.22 1.23
EXAMPLES:
$0 # Test all Go versions in parallel
$0 --sequential # Test all Go versions sequentially
$0 1.21 1.22 1.23 # Test specific versions
$0 --verbose --output ./results 1.25 1.26 # Verbose output to custom directory
EXIT CODES:
0 Recent Go versions pass (good compatibility)
1 Recent Go versions fail (needs attention) or script error
EOF
}
log() {
echo -e "${BLUE}[$(date +'%H:%M:%S')]${NC} $*" >&2
}
log_success() {
echo -e "${GREEN}[$(date +'%H:%M:%S')] ✓${NC} $*" >&2
}
log_error() {
echo -e "${RED}[$(date +'%H:%M:%S')] ✗${NC} $*" >&2
}
log_warning() {
echo -e "${YELLOW}[$(date +'%H:%M:%S')] ⚠${NC} $*" >&2
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
exit 0
;;
-s|--sequential)
PARALLEL=false
shift
;;
-v|--verbose)
VERBOSE=true
shift
;;
-o|--output)
OUTPUT_DIR="$2"
shift 2
;;
-t|--timeout)
DOCKER_TIMEOUT="$2"
shift 2
;;
--list)
echo "Available Go versions:"
printf '%s\n' "${GO_VERSIONS[@]}"
exit 0
;;
-*)
echo "Unknown option: $1" >&2
usage
exit 1
;;
*)
# Remaining arguments are Go versions
break
;;
esac
done
# If specific versions provided, use those instead of defaults
if [[ $# -gt 0 ]]; then
GO_VERSIONS=("$@")
fi
# Validate Go versions
for version in "${GO_VERSIONS[@]}"; do
if ! [[ "$version" =~ ^1\.(1[1-9]|2[0-6])$ ]]; then
log_error "Invalid Go version: $version. Supported versions: 1.11-1.26"
exit 1
fi
done
# Check if Docker is available
if ! command -v docker &> /dev/null; then
log_error "Docker is required but not installed or not in PATH"
exit 1
fi
# Check if Docker daemon is running
if ! docker info &> /dev/null; then
log_error "Docker daemon is not running"
exit 1
fi
# Create output directory
mkdir -p "$OUTPUT_DIR"
# Function to test a single Go version
test_go_version() {
local go_version="$1"
local container_name="go-toml-test-${go_version}"
local result_file="${OUTPUT_DIR}/go-${go_version}.txt"
local dockerfile_content
log "Testing Go $go_version..."
# Create a temporary Dockerfile for this version
# For Go versions < 1.21, we need to update go.mod to match the Go version
local needs_go_mod_update=false
if [[ $(echo "$go_version 1.21" | tr ' ' '\n' | sort -V | head -n1) == "$go_version" && "$go_version" != "1.21" ]]; then
needs_go_mod_update=true
fi
dockerfile_content="FROM golang:${go_version}-alpine
# Install git (required for go mod)
RUN apk add --no-cache git
# Set working directory
WORKDIR /app
# Copy source code
COPY . ."
# Add go.mod update step for older Go versions
if [[ "$needs_go_mod_update" == true ]]; then
dockerfile_content="$dockerfile_content
# Update go.mod to match Go version (required for Go < 1.21)
RUN if [ -f go.mod ]; then sed -i 's/^go [0-9]\\+\\.[0-9]\\+\\(\\.[0-9]\\+\\)\\?/go $go_version/' go.mod; fi
# Note: Go versions < 1.21 may fail due to missing standard library packages (e.g., slices)
# This is expected for projects that use Go 1.21+ features"
fi
dockerfile_content="$dockerfile_content
# Run tests
CMD [\"sh\", \"-c\", \"go version && echo '--- Running go test ./... ---' && go test ./...\"]"
# Create temporary directory for this test
local temp_dir
temp_dir=$(mktemp -d)
# Copy source to temp directory (excluding test results and git)
rsync -a --exclude="$OUTPUT_DIR" --exclude=".git" --exclude="*.test" . "$temp_dir/"
# Create Dockerfile in temp directory
echo "$dockerfile_content" > "$temp_dir/Dockerfile"
# Build and run container
local exit_code=0
local output
if $VERBOSE; then
log "Building Docker image for Go $go_version..."
fi
# Capture both stdout and stderr, and the exit code
if output=$(cd "$temp_dir" && timeout "$DOCKER_TIMEOUT" docker build -t "$container_name" . 2>&1 && \
timeout "$DOCKER_TIMEOUT" docker run --rm "$container_name" 2>&1); then
log_success "Go $go_version: PASSED"
echo "PASSED" > "${result_file}.status"
else
exit_code=$?
log_error "Go $go_version: FAILED (exit code: $exit_code)"
echo "FAILED" > "${result_file}.status"
fi
# Save full output
echo "$output" > "$result_file"
# Clean up
docker rmi "$container_name" &> /dev/null || true
rm -rf "$temp_dir"
if $VERBOSE; then
echo "--- Go $go_version output ---"
echo "$output"
echo "--- End Go $go_version output ---"
fi
return $exit_code
}
# Function to run tests in parallel
run_parallel() {
local pids=()
local failed_versions=()
log "Starting parallel tests for ${#GO_VERSIONS[@]} Go versions..."
# Start all tests in background
for version in "${GO_VERSIONS[@]}"; do
test_go_version "$version" &
pids+=($!)
done
# Wait for all tests to complete
for i in "${!pids[@]}"; do
local pid=${pids[$i]}
local version=${GO_VERSIONS[$i]}
if ! wait $pid; then
failed_versions+=("$version")
fi
done
return ${#failed_versions[@]}
}
# Function to run tests sequentially
run_sequential() {
local failed_versions=()
log "Starting sequential tests for ${#GO_VERSIONS[@]} Go versions..."
for version in "${GO_VERSIONS[@]}"; do
if ! test_go_version "$version"; then
failed_versions+=("$version")
fi
done
return ${#failed_versions[@]}
}
# Main execution
main() {
local start_time
start_time=$(date +%s)
log "Starting Go version compatibility tests..."
log "Testing versions: ${GO_VERSIONS[*]}"
log "Output directory: $OUTPUT_DIR"
log "Parallel execution: $PARALLEL"
local failed_count
if $PARALLEL; then
run_parallel
failed_count=$?
else
run_sequential
failed_count=$?
fi
local end_time
end_time=$(date +%s)
local duration=$((end_time - start_time))
# Collect results for display
local passed_versions=()
local failed_versions=()
local unknown_versions=()
local passed_count=0
for version in "${GO_VERSIONS[@]}"; do
local status_file="${OUTPUT_DIR}/go-${version}.txt.status"
if [[ -f "$status_file" ]]; then
local status
status=$(cat "$status_file")
if [[ "$status" == "PASSED" ]]; then
passed_versions+=("$version")
((passed_count++))
else
failed_versions+=("$version")
fi
else
unknown_versions+=("$version")
fi
done
# Generate summary report
local summary_file="${OUTPUT_DIR}/summary.txt"
{
echo "Go Version Compatibility Test Summary"
echo "====================================="
echo "Date: $(date)"
echo "Duration: ${duration}s"
echo "Parallel: $PARALLEL"
echo ""
echo "Results:"
for version in "${GO_VERSIONS[@]}"; do
local status_file="${OUTPUT_DIR}/go-${version}.txt.status"
if [[ -f "$status_file" ]]; then
local status
status=$(cat "$status_file")
if [[ "$status" == "PASSED" ]]; then
echo " Go $version: ✓ PASSED"
else
echo " Go $version: ✗ FAILED"
fi
else
echo " Go $version: ? UNKNOWN (no status file)"
fi
done
echo ""
echo "Summary: $passed_count/${#GO_VERSIONS[@]} versions passed"
if [[ $failed_count -gt 0 ]]; then
echo ""
echo "Failed versions details:"
for version in "${failed_versions[@]}"; do
echo ""
echo "--- Go $version (FAILED) ---"
local result_file="${OUTPUT_DIR}/go-${version}.txt"
if [[ -f "$result_file" ]]; then
tail -n 30 "$result_file"
fi
done
fi
} > "$summary_file"
# Find lowest continuous supported version and check recent versions
local lowest_continuous_version=""
local recent_versions_failed=false
# Sort versions to ensure proper order
local sorted_versions=()
for version in "${GO_VERSIONS[@]}"; do
sorted_versions+=("$version")
done
# Sort versions numerically (1.11, 1.12, ..., 1.25)
IFS=$'\n' sorted_versions=($(sort -V <<< "${sorted_versions[*]}"))
# Find lowest continuous supported version (all versions from this point onwards pass)
for version in "${sorted_versions[@]}"; do
local status_file="${OUTPUT_DIR}/go-${version}.txt.status"
local all_subsequent_pass=true
# Check if this version and all subsequent versions pass
local found_current=false
for check_version in "${sorted_versions[@]}"; do
if [[ "$check_version" == "$version" ]]; then
found_current=true
fi
if [[ "$found_current" == true ]]; then
local check_status_file="${OUTPUT_DIR}/go-${check_version}.txt.status"
if [[ -f "$check_status_file" ]]; then
local status
status=$(cat "$check_status_file")
if [[ "$status" != "PASSED" ]]; then
all_subsequent_pass=false
break
fi
else
all_subsequent_pass=false
break
fi
fi
done
if [[ "$all_subsequent_pass" == true ]]; then
lowest_continuous_version="$version"
break
fi
done
# Check if the two most recent versions failed
local num_versions=${#sorted_versions[@]}
if [[ $num_versions -ge 2 ]]; then
local second_recent="${sorted_versions[$((num_versions-2))]}"
local most_recent="${sorted_versions[$((num_versions-1))]}"
local second_recent_status_file="${OUTPUT_DIR}/go-${second_recent}.txt.status"
local most_recent_status_file="${OUTPUT_DIR}/go-${most_recent}.txt.status"
local second_recent_failed=false
local most_recent_failed=false
if [[ -f "$second_recent_status_file" ]]; then
local status
status=$(cat "$second_recent_status_file")
if [[ "$status" != "PASSED" ]]; then
second_recent_failed=true
fi
else
second_recent_failed=true
fi
if [[ -f "$most_recent_status_file" ]]; then
local status
status=$(cat "$most_recent_status_file")
if [[ "$status" != "PASSED" ]]; then
most_recent_failed=true
fi
else
most_recent_failed=true
fi
if [[ "$second_recent_failed" == true || "$most_recent_failed" == true ]]; then
recent_versions_failed=true
fi
elif [[ $num_versions -eq 1 ]]; then
# Only one version tested, check if it's the most recent and failed
local only_version="${sorted_versions[0]}"
local only_status_file="${OUTPUT_DIR}/go-${only_version}.txt.status"
if [[ -f "$only_status_file" ]]; then
local status
status=$(cat "$only_status_file")
if [[ "$status" != "PASSED" ]]; then
recent_versions_failed=true
fi
else
recent_versions_failed=true
fi
fi
# Display summary
echo ""
log "Test completed in ${duration}s"
log "Summary report: $summary_file"
echo ""
echo "========================================"
echo " FINAL RESULTS"
echo "========================================"
echo ""
# Display passed versions
if [[ ${#passed_versions[@]} -gt 0 ]]; then
log_success "PASSED (${#passed_versions[@]}/${#GO_VERSIONS[@]}):"
# Sort passed versions for display
local sorted_passed=()
for version in "${sorted_versions[@]}"; do
for passed_version in "${passed_versions[@]}"; do
if [[ "$version" == "$passed_version" ]]; then
sorted_passed+=("$version")
break
fi
done
done
for version in "${sorted_passed[@]}"; do
echo -e " ${GREEN}${NC} Go $version"
done
echo ""
fi
# Display failed versions
if [[ ${#failed_versions[@]} -gt 0 ]]; then
log_error "FAILED (${#failed_versions[@]}/${#GO_VERSIONS[@]}):"
# Sort failed versions for display
local sorted_failed=()
for version in "${sorted_versions[@]}"; do
for failed_version in "${failed_versions[@]}"; do
if [[ "$version" == "$failed_version" ]]; then
sorted_failed+=("$version")
break
fi
done
done
for version in "${sorted_failed[@]}"; do
echo -e " ${RED}${NC} Go $version"
done
echo ""
# Show failure details
echo "========================================"
echo " FAILURE DETAILS"
echo "========================================"
echo ""
for version in "${sorted_failed[@]}"; do
echo -e "${RED}--- Go $version FAILURE LOGS (last 30 lines) ---${NC}"
local result_file="${OUTPUT_DIR}/go-${version}.txt"
if [[ -f "$result_file" ]]; then
tail -n 30 "$result_file" | sed 's/^/ /'
else
echo " No log file found: $result_file"
fi
echo ""
done
fi
# Display unknown versions
if [[ ${#unknown_versions[@]} -gt 0 ]]; then
log_warning "UNKNOWN (${#unknown_versions[@]}/${#GO_VERSIONS[@]}):"
for version in "${unknown_versions[@]}"; do
echo -e " ${YELLOW}?${NC} Go $version (no status file)"
done
echo ""
fi
echo "========================================"
echo " COMPATIBILITY SUMMARY"
echo "========================================"
echo ""
if [[ -n "$lowest_continuous_version" ]]; then
log_success "Lowest continuous supported version: Go $lowest_continuous_version"
echo " (All versions from Go $lowest_continuous_version onwards pass)"
else
log_error "No continuous version support found"
echo " (No version has all subsequent versions passing)"
fi
echo ""
echo "========================================"
echo "Full detailed logs available in: $OUTPUT_DIR"
echo "========================================"
# Determine exit code based on recent versions
if [[ "$recent_versions_failed" == true ]]; then
log_error "OVERALL RESULT: Recent Go versions failed - this needs attention!"
if [[ -n "$lowest_continuous_version" ]]; then
echo "Note: Continuous support starts from Go $lowest_continuous_version"
fi
exit 1
else
log_success "OVERALL RESULT: Recent Go versions pass - compatibility looks good!"
if [[ -n "$lowest_continuous_version" ]]; then
echo "Continuous support starts from Go $lowest_continuous_version"
fi
exit 0
fi
}
# Trap to clean up on exit
cleanup() {
# Kill any remaining background processes
jobs -p | xargs -r kill 2>/dev/null || true
# Clean up any remaining Docker containers
docker ps -q --filter "name=go-toml-test-" | xargs -r docker stop 2>/dev/null || true
docker images -q --filter "reference=go-toml-test-*" | xargs -r docker rmi 2>/dev/null || true
}
trap cleanup EXIT
# Run main function
main
+15 -6
View File
@@ -6,9 +6,18 @@ import (
"time"
)
var timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
var textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
var textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
var mapStringInterfaceType = reflect.TypeOf(map[string]interface{}(nil))
var sliceInterfaceType = reflect.TypeOf([]interface{}(nil))
var stringType = reflect.TypeOf("")
// isZeroer is used to check if a type has a custom IsZero method.
// This allows custom types to define their own zero-value semantics.
type isZeroer interface {
IsZero() bool
}
var (
timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
isZeroerType = reflect.TypeOf((*isZeroer)(nil)).Elem()
mapStringInterfaceType = reflect.TypeOf(map[string]interface{}(nil))
sliceInterfaceType = reflect.TypeOf([]interface{}(nil))
stringType = reflect.TypeOf("")
)
+165 -41
View File
@@ -12,7 +12,6 @@ import (
"sync/atomic"
"time"
"github.com/pelletier/go-toml/v2/internal/danger"
"github.com/pelletier/go-toml/v2/internal/tracker"
"github.com/pelletier/go-toml/v2/unstable"
)
@@ -57,13 +56,18 @@ func (d *Decoder) DisallowUnknownFields() *Decoder {
// EnableUnmarshalerInterface allows to enable unmarshaler interface.
//
// With this feature enabled, types implementing the unstable/Unmarshaler
// With this feature enabled, types implementing the unstable.Unmarshaler
// interface can be decoded from any structure of the document. It allows types
// that don't have a straightforward TOML representation to provide their own
// decoding logic.
//
// Currently, types can only decode from a single value. Tables and array tables
// are not supported.
// The UnmarshalTOML method receives raw TOML bytes:
// - For single values: the raw value bytes (e.g., `"hello"` for a string)
// - For tables: all key-value lines belonging to that table
// - For inline tables/arrays: the raw bytes of the inline structure
//
// The unstable.RawMessage type can be used to capture raw TOML bytes for
// later processing, similar to json.RawMessage.
//
// *Unstable:* This method does not follow the compatibility guarantees of
// semver. It can be changed or removed without a new major version being
@@ -123,6 +127,7 @@ func (d *Decoder) Decode(v interface{}) error {
dec := decoder{
strict: strict{
Enabled: d.strict,
doc: b,
},
unmarshalerInterface: d.unmarshalerInterface,
}
@@ -226,7 +231,7 @@ func (d *decoder) FromParser(v interface{}) error {
}
if r.IsNil() {
return fmt.Errorf("toml: decoding pointer target cannot be nil")
return errors.New("toml: decoding pointer target cannot be nil")
}
r = r.Elem()
@@ -273,7 +278,7 @@ func (d *decoder) handleRootExpression(expr *unstable.Node, v reflect.Value) err
var err error
var first bool // used for to clear array tables on first use
if !(d.skipUntilTable && expr.Kind == unstable.KeyValue) {
if !d.skipUntilTable || expr.Kind != unstable.KeyValue {
first, err = d.seen.CheckExpression(expr)
if err != nil {
return err
@@ -378,7 +383,7 @@ func (d *decoder) handleArrayTableCollectionLast(key unstable.Iterator, v reflec
case reflect.Array:
idx := d.arrayIndex(true, v)
if idx >= v.Len() {
return v, fmt.Errorf("%s at position %d", d.typeMismatchError("array table", v.Type()), idx)
return v, fmt.Errorf("%w at position %d", d.typeMismatchError("array table", v.Type()), idx)
}
elem := v.Index(idx)
_, err := d.handleArrayTable(key, elem)
@@ -416,27 +421,51 @@ func (d *decoder) handleArrayTableCollection(key unstable.Iterator, v reflect.Va
return v, nil
case reflect.Slice:
elem := v.Index(v.Len() - 1)
// Create a new element when the slice is empty; otherwise operate on
// the last element.
var (
elem reflect.Value
created bool
)
if v.Len() == 0 {
created = true
elemType := v.Type().Elem()
if elemType.Kind() == reflect.Interface {
elem = makeMapStringInterface()
} else {
elem = reflect.New(elemType).Elem()
}
} else {
elem = v.Index(v.Len() - 1)
}
x, err := d.handleArrayTable(key, elem)
if err != nil || d.skipUntilTable {
return reflect.Value{}, err
}
if x.IsValid() {
if created {
elem = x
} else {
elem.Set(x)
}
}
if created {
return reflect.Append(v, elem), nil
}
return v, err
case reflect.Array:
idx := d.arrayIndex(false, v)
if idx >= v.Len() {
return v, fmt.Errorf("%s at position %d", d.typeMismatchError("array table", v.Type()), idx)
return v, fmt.Errorf("%w at position %d", d.typeMismatchError("array table", v.Type()), idx)
}
elem := v.Index(idx)
_, err := d.handleArrayTable(key, elem)
return v, err
}
default:
return d.handleArrayTable(key, v)
}
}
func (d *decoder) handleKeyPart(key unstable.Iterator, v reflect.Value, nextFn handlerFn, makeFn valueMakerFn) (reflect.Value, error) {
@@ -470,7 +499,8 @@ func (d *decoder) handleKeyPart(key unstable.Iterator, v reflect.Value, nextFn h
mv := v.MapIndex(mk)
set := false
if !mv.IsValid() {
switch {
case !mv.IsValid():
// If there is no value in the map, create a new one according to
// the map type. If the element type is interface, create either a
// map[string]interface{} or a []interface{} depending on whether
@@ -483,13 +513,13 @@ func (d *decoder) handleKeyPart(key unstable.Iterator, v reflect.Value, nextFn h
mv = reflect.New(t).Elem()
}
set = true
} else if mv.Kind() == reflect.Interface {
case mv.Kind() == reflect.Interface:
mv = mv.Elem()
if !mv.IsValid() {
mv = makeFn()
}
set = true
} else if !mv.CanAddr() {
case !mv.CanAddr():
vt := v.Type()
t := vt.Elem()
oldmv := mv
@@ -574,9 +604,8 @@ func (d *decoder) handleArrayTablePart(key unstable.Iterator, v reflect.Value) (
// cannot handle it.
func (d *decoder) handleTable(key unstable.Iterator, v reflect.Value) (reflect.Value, error) {
if v.Kind() == reflect.Slice {
if v.Len() == 0 {
return reflect.Value{}, unstable.NewParserError(key.Node().Data, "cannot store a table in a slice")
}
// For non-empty slices, work with the last element
if v.Len() > 0 {
elem := v.Index(v.Len() - 1)
x, err := d.handleTable(key, elem)
if err != nil {
@@ -587,6 +616,17 @@ func (d *decoder) handleTable(key unstable.Iterator, v reflect.Value) (reflect.V
}
return reflect.Value{}, nil
}
// Empty slice - check if it implements Unmarshaler (e.g., RawMessage)
// and we're at the end of the key path
if d.unmarshalerInterface && !key.Next() {
if v.CanAddr() && v.Addr().CanInterface() {
if outi, ok := v.Addr().Interface().(unstable.Unmarshaler); ok {
return d.handleKeyValuesUnmarshaler(outi)
}
}
}
return reflect.Value{}, unstable.NewParserError(key.Node().Data, "cannot store a table in a slice")
}
if key.Next() {
// Still scoping the key
return d.handleTablePart(key, v)
@@ -599,6 +639,24 @@ func (d *decoder) handleTable(key unstable.Iterator, v reflect.Value) (reflect.V
// Handle root expressions until the end of the document or the next
// non-key-value.
func (d *decoder) handleKeyValues(v reflect.Value) (reflect.Value, error) {
// Check if target implements Unmarshaler before processing key-values.
// This allows types to handle entire tables themselves.
if d.unmarshalerInterface {
vv := v
for vv.Kind() == reflect.Ptr {
if vv.IsNil() {
vv.Set(reflect.New(vv.Type().Elem()))
}
vv = vv.Elem()
}
if vv.CanAddr() && vv.Addr().CanInterface() {
if outi, ok := vv.Addr().Interface().(unstable.Unmarshaler); ok {
// Collect all key-value expressions for this table
return d.handleKeyValuesUnmarshaler(outi)
}
}
}
var rv reflect.Value
for d.nextExpr() {
expr := d.expr()
@@ -628,6 +686,41 @@ func (d *decoder) handleKeyValues(v reflect.Value) (reflect.Value, error) {
return rv, nil
}
// handleKeyValuesUnmarshaler collects all key-value expressions for a table
// and passes them to the Unmarshaler as raw TOML bytes.
func (d *decoder) handleKeyValuesUnmarshaler(u unstable.Unmarshaler) (reflect.Value, error) {
// Collect raw bytes from all key-value expressions for this table.
// We use the Raw field on each KeyValue expression to preserve the
// original formatting (whitespace, quoting style, etc.) from the document.
var buf []byte
for d.nextExpr() {
expr := d.expr()
if expr.Kind != unstable.KeyValue {
d.stashExpr()
break
}
_, err := d.seen.CheckExpression(expr)
if err != nil {
return reflect.Value{}, err
}
// Use the raw bytes from the original document to preserve formatting
if expr.Raw.Length > 0 {
raw := d.p.Raw(expr.Raw)
buf = append(buf, raw...)
}
buf = append(buf, '\n')
}
if err := u.UnmarshalTOML(buf); err != nil {
return reflect.Value{}, err
}
return reflect.Value{}, nil
}
type (
handlerFn func(key unstable.Iterator, v reflect.Value) (reflect.Value, error)
valueMakerFn func() reflect.Value
@@ -672,15 +765,22 @@ func (d *decoder) handleValue(value *unstable.Node, v reflect.Value) error {
if d.unmarshalerInterface {
if v.CanAddr() && v.Addr().CanInterface() {
if outi, ok := v.Addr().Interface().(unstable.Unmarshaler); ok {
return outi.UnmarshalTOML(value)
// Pass raw bytes from the original document
return outi.UnmarshalTOML(d.p.Raw(value.Raw))
}
}
}
// Only try TextUnmarshaler for scalar types. For Array and InlineTable,
// fall through to struct/map unmarshaling to allow flexible unmarshaling
// where a type can implement UnmarshalText for string values but still
// be populated field-by-field from a table. See issue #974.
if value.Kind != unstable.Array && value.Kind != unstable.InlineTable {
ok, err := d.tryTextUnmarshaler(value, v)
if ok || err != nil {
return err
}
}
switch value.Kind {
case unstable.String:
@@ -821,6 +921,9 @@ func (d *decoder) unmarshalDateTime(value *unstable.Node, v reflect.Value) error
return err
}
if v.Kind() != reflect.Interface && v.Type() != timeType {
return unstable.NewParserError(d.p.Raw(value.Raw), "%s", d.typeMismatchString("datetime", v.Type()))
}
v.Set(reflect.ValueOf(dt))
return nil
}
@@ -831,14 +934,14 @@ func (d *decoder) unmarshalLocalDate(value *unstable.Node, v reflect.Value) erro
return err
}
if v.Kind() != reflect.Interface && v.Type() != timeType {
return unstable.NewParserError(d.p.Raw(value.Raw), "%s", d.typeMismatchString("local date", v.Type()))
}
if v.Type() == timeType {
cast := ld.AsTime(time.Local)
v.Set(reflect.ValueOf(cast))
v.Set(reflect.ValueOf(ld.AsTime(time.Local)))
return nil
}
v.Set(reflect.ValueOf(ld))
return nil
}
@@ -852,6 +955,9 @@ func (d *decoder) unmarshalLocalTime(value *unstable.Node, v reflect.Value) erro
return unstable.NewParserError(rest, "extra characters at the end of a local time")
}
if v.Kind() != reflect.Interface {
return unstable.NewParserError(d.p.Raw(value.Raw), "%s", d.typeMismatchString("local time", v.Type()))
}
v.Set(reflect.ValueOf(lt))
return nil
}
@@ -866,15 +972,14 @@ func (d *decoder) unmarshalLocalDateTime(value *unstable.Node, v reflect.Value)
return unstable.NewParserError(rest, "extra characters at the end of a local date time")
}
if v.Kind() != reflect.Interface && v.Type() != timeType {
return unstable.NewParserError(d.p.Raw(value.Raw), "%s", d.typeMismatchString("local datetime", v.Type()))
}
if v.Type() == timeType {
cast := ldt.AsTime(time.Local)
v.Set(reflect.ValueOf(cast))
v.Set(reflect.ValueOf(ldt.AsTime(time.Local)))
return nil
}
v.Set(reflect.ValueOf(ldt))
return nil
}
@@ -929,8 +1034,9 @@ const (
// compile time, so it is computed during initialization.
var maxUint int64 = math.MaxInt64
func init() {
func init() { //nolint:gochecknoinits
m := uint64(^uint(0))
// #nosec G115
if m < uint64(maxUint) {
maxUint = int64(m)
}
@@ -1010,7 +1116,7 @@ func (d *decoder) unmarshalInteger(value *unstable.Node, v reflect.Value) error
case reflect.Interface:
r = reflect.ValueOf(i)
default:
return unstable.NewParserError(d.p.Raw(value.Raw), d.typeMismatchString("integer", v.Type()))
return unstable.NewParserError(d.p.Raw(value.Raw), "%s", d.typeMismatchString("integer", v.Type()))
}
if !r.Type().AssignableTo(v.Type()) {
@@ -1029,7 +1135,7 @@ func (d *decoder) unmarshalString(value *unstable.Node, v reflect.Value) error {
case reflect.Interface:
v.Set(reflect.ValueOf(string(value.Data)))
default:
return unstable.NewParserError(d.p.Raw(value.Raw), d.typeMismatchString("string", v.Type()))
return unstable.NewParserError(d.p.Raw(value.Raw), "%s", d.typeMismatchString("string", v.Type()))
}
return nil
@@ -1080,35 +1186,39 @@ func (d *decoder) keyFromData(keyType reflect.Type, data []byte) (reflect.Value,
return reflect.Value{}, fmt.Errorf("toml: error unmarshalling key type %s from text: %w", stringType, err)
}
return mk.Elem(), nil
}
case keyType.Kind() == reflect.Int || keyType.Kind() == reflect.Int8 || keyType.Kind() == reflect.Int16 || keyType.Kind() == reflect.Int32 || keyType.Kind() == reflect.Int64:
switch keyType.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
key, err := strconv.ParseInt(string(data), 10, 64)
if err != nil {
return reflect.Value{}, fmt.Errorf("toml: error parsing key of type %s from integer: %w", stringType, err)
}
return reflect.ValueOf(key).Convert(keyType), nil
case keyType.Kind() == reflect.Uint || keyType.Kind() == reflect.Uint8 || keyType.Kind() == reflect.Uint16 || keyType.Kind() == reflect.Uint32 || keyType.Kind() == reflect.Uint64:
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
key, err := strconv.ParseUint(string(data), 10, 64)
if err != nil {
return reflect.Value{}, fmt.Errorf("toml: error parsing key of type %s from unsigned integer: %w", stringType, err)
}
return reflect.ValueOf(key).Convert(keyType), nil
case keyType.Kind() == reflect.Float32:
case reflect.Float32:
key, err := strconv.ParseFloat(string(data), 32)
if err != nil {
return reflect.Value{}, fmt.Errorf("toml: error parsing key of type %s from float: %w", stringType, err)
}
return reflect.ValueOf(float32(key)), nil
case keyType.Kind() == reflect.Float64:
case reflect.Float64:
key, err := strconv.ParseFloat(string(data), 64)
if err != nil {
return reflect.Value{}, fmt.Errorf("toml: error parsing key of type %s from float: %w", stringType, err)
}
return reflect.ValueOf(float64(key)), nil
}
default:
return reflect.Value{}, fmt.Errorf("toml: cannot convert map key of type %s to expected type %s", stringType, keyType)
}
}
func (d *decoder) handleKeyValuePart(key unstable.Iterator, value *unstable.Node, v reflect.Value) (reflect.Value, error) {
@@ -1154,6 +1264,18 @@ func (d *decoder) handleKeyValuePart(key unstable.Iterator, value *unstable.Node
case reflect.Struct:
path, found := structFieldPath(v, string(key.Node().Data))
if !found {
// If no matching struct field is found but the target implements the
// unstable.Unmarshaler interface (and it is enabled), delegate the
// decoding of this value to the custom unmarshaler.
if d.unmarshalerInterface {
if v.CanAddr() && v.Addr().CanInterface() {
if outi, ok := v.Addr().Interface().(unstable.Unmarshaler); ok {
// Pass raw bytes from the original document
return reflect.Value{}, outi.UnmarshalTOML(d.p.Raw(value.Raw))
}
}
}
// Otherwise, keep previous behavior and skip until the next table.
d.skipUntilTable = true
break
}
@@ -1259,13 +1381,13 @@ func fieldByIndex(v reflect.Value, path []int) reflect.Value {
type fieldPathsMap = map[string][]int
var globalFieldPathsCache atomic.Value // map[danger.TypeID]fieldPathsMap
var globalFieldPathsCache atomic.Value // map[reflect.Type]fieldPathsMap
func structFieldPath(v reflect.Value, name string) ([]int, bool) {
t := v.Type()
cache, _ := globalFieldPathsCache.Load().(map[danger.TypeID]fieldPathsMap)
fieldPaths, ok := cache[danger.MakeTypeID(t)]
cache, _ := globalFieldPathsCache.Load().(map[reflect.Type]fieldPathsMap)
fieldPaths, ok := cache[t]
if !ok {
fieldPaths = map[string][]int{}
@@ -1276,8 +1398,8 @@ func structFieldPath(v reflect.Value, name string) ([]int, bool) {
fieldPaths[strings.ToLower(name)] = path
})
newCache := make(map[danger.TypeID]fieldPathsMap, len(cache)+1)
newCache[danger.MakeTypeID(t)] = fieldPaths
newCache := make(map[reflect.Type]fieldPathsMap, len(cache)+1)
newCache[t] = fieldPaths
for k, v := range cache {
newCache[k] = v
}
@@ -1301,7 +1423,9 @@ func forEachField(t reflect.Type, path []int, do func(name string, path []int))
continue
}
fieldPath := append(path, i)
fieldPath := make([]int, 0, len(path)+1)
fieldPath = append(fieldPath, path...)
fieldPath = append(fieldPath, i)
fieldPath = fieldPath[:len(fieldPath):len(fieldPath)]
name := f.Tag.Get("toml")
+42 -29
View File
@@ -1,10 +1,8 @@
package unstable
import (
"errors"
"fmt"
"unsafe"
"github.com/pelletier/go-toml/v2/internal/danger"
)
// Iterator over a sequence of nodes.
@@ -19,30 +17,43 @@ import (
// // do something with n
// }
type Iterator struct {
nodes *[]Node
idx int32
started bool
node *Node
}
// Next moves the iterator forward and returns true if points to a
// node, false otherwise.
func (c *Iterator) Next() bool {
if c.nodes == nil {
return false
}
nodes := *c.nodes
if !c.started {
c.started = true
} else if c.node.Valid() {
c.node = c.node.Next()
} else {
idx := c.idx
if idx >= 0 && int(idx) < len(nodes) {
c.idx = nodes[idx].next
}
return c.node.Valid()
}
return c.idx >= 0 && int(c.idx) < len(nodes)
}
// IsLast returns true if the current node of the iterator is the last
// one. Subsequent calls to Next() will return false.
func (c *Iterator) IsLast() bool {
return c.node.next == 0
return c.nodes == nil || c.idx < 0 || (*c.nodes)[c.idx].next < 0
}
// Node returns a pointer to the node pointed at by the iterator.
func (c *Iterator) Node() *Node {
return c.node
if c.nodes == nil || c.idx < 0 {
return nil
}
n := &(*c.nodes)[c.idx]
n.nodes = c.nodes
return n
}
// Node in a TOML expression AST.
@@ -65,11 +76,12 @@ type Node struct {
Raw Range // Raw bytes from the input.
Data []byte // Node value (either allocated or referencing the input).
// References to other nodes, as offsets in the backing array
// from this node. References can go backward, so those can be
// negative.
next int // 0 if last element
child int // 0 if no child
// Absolute indices into the backing nodes slice. -1 means none.
next int32
child int32
// Reference to the backing nodes slice for navigation.
nodes *[]Node
}
// Range of bytes in the document.
@@ -80,24 +92,24 @@ type Range struct {
// Next returns a pointer to the next node, or nil if there is no next node.
func (n *Node) Next() *Node {
if n.next == 0 {
if n.next < 0 {
return nil
}
ptr := unsafe.Pointer(n)
size := unsafe.Sizeof(Node{})
return (*Node)(danger.Stride(ptr, size, n.next))
next := &(*n.nodes)[n.next]
next.nodes = n.nodes
return next
}
// Child returns a pointer to the first child node of this node. Other children
// can be accessed calling Next on the first child. Returns an nil if this Node
// can be accessed calling Next on the first child. Returns nil if this Node
// has no child.
func (n *Node) Child() *Node {
if n.child == 0 {
if n.child < 0 {
return nil
}
ptr := unsafe.Pointer(n)
size := unsafe.Sizeof(Node{})
return (*Node)(danger.Stride(ptr, size, n.child))
child := &(*n.nodes)[n.child]
child.nodes = n.nodes
return child
}
// Valid returns true if the node's kind is set (not to Invalid).
@@ -111,13 +123,14 @@ func (n *Node) Valid() bool {
func (n *Node) Key() Iterator {
switch n.Kind {
case KeyValue:
value := n.Child()
if !value.Valid() {
panic(fmt.Errorf("KeyValue should have at least two children"))
child := n.child
if child < 0 {
panic(errors.New("KeyValue should have at least two children"))
}
return Iterator{node: value.Next()}
valueNode := &(*n.nodes)[child]
return Iterator{nodes: n.nodes, idx: valueNode.next}
case Table, ArrayTable:
return Iterator{node: n.Child()}
return Iterator{nodes: n.nodes, idx: n.child}
default:
panic(fmt.Errorf("Key() is not supported on a %s", n.Kind))
}
@@ -132,5 +145,5 @@ func (n *Node) Value() *Node {
// Children returns an iterator over a node's children.
func (n *Node) Children() Iterator {
return Iterator{node: n.Child()}
return Iterator{nodes: n.nodes, idx: n.child}
}
+10 -17
View File
@@ -7,15 +7,6 @@ type root struct {
nodes []Node
}
// Iterator over the top level nodes.
func (r *root) Iterator() Iterator {
it := Iterator{}
if len(r.nodes) > 0 {
it.node = &r.nodes[0]
}
return it
}
func (r *root) at(idx reference) *Node {
return &r.nodes[idx]
}
@@ -33,12 +24,10 @@ type builder struct {
lastIdx int
}
func (b *builder) Tree() *root {
return &b.tree
}
func (b *builder) NodeAt(ref reference) *Node {
return b.tree.at(ref)
n := b.tree.at(ref)
n.nodes = &b.tree.nodes
return n
}
func (b *builder) Reset() {
@@ -48,24 +37,28 @@ func (b *builder) Reset() {
func (b *builder) Push(n Node) reference {
b.lastIdx = len(b.tree.nodes)
n.next = -1
n.child = -1
b.tree.nodes = append(b.tree.nodes, n)
return reference(b.lastIdx)
}
func (b *builder) PushAndChain(n Node) reference {
newIdx := len(b.tree.nodes)
n.next = -1
n.child = -1
b.tree.nodes = append(b.tree.nodes, n)
if b.lastIdx >= 0 {
b.tree.nodes[b.lastIdx].next = newIdx - b.lastIdx
b.tree.nodes[b.lastIdx].next = int32(newIdx) //nolint:gosec // TOML ASTs are small
}
b.lastIdx = newIdx
return reference(b.lastIdx)
}
func (b *builder) AttachChild(parent reference, child reference) {
b.tree.nodes[parent].child = int(child) - int(parent)
b.tree.nodes[parent].child = int32(child) //nolint:gosec // TOML ASTs are small
}
func (b *builder) Chain(from reference, to reference) {
b.tree.nodes[from].next = int(to) - int(from)
b.tree.nodes[from].next = int32(to) //nolint:gosec // TOML ASTs are small
}
+16 -4
View File
@@ -6,28 +6,40 @@ import "fmt"
type Kind int
const (
// Meta
// Invalid represents an invalid meta node.
Invalid Kind = iota
// Comment represents a comment meta node.
Comment
// Key represents a key meta node.
Key
// Top level structures
// Table represents a top-level table.
Table
// ArrayTable represents a top-level array table.
ArrayTable
// KeyValue represents a top-level key value.
KeyValue
// Containers values
// Array represents an array container value.
Array
// InlineTable represents an inline table container value.
InlineTable
// Values
// String represents a string value.
String
// Bool represents a boolean value.
Bool
// Float represents a floating point value.
Float
// Integer represents an integer value.
Integer
// LocalDate represents a a local date value.
LocalDate
// LocalTime represents a local time value.
LocalTime
// LocalDateTime represents a local date/time value.
LocalDateTime
// DateTime represents a data/time value.
DateTime
)
+71 -41
View File
@@ -3,10 +3,10 @@ package unstable
import (
"bytes"
"fmt"
"reflect"
"unicode"
"github.com/pelletier/go-toml/v2/internal/characters"
"github.com/pelletier/go-toml/v2/internal/danger"
)
// ParserError describes an error relative to the content of the document.
@@ -70,11 +70,34 @@ func (p *Parser) Data() []byte {
// panics.
func (p *Parser) Range(b []byte) Range {
return Range{
Offset: uint32(danger.SubsliceOffset(p.data, b)),
Length: uint32(len(b)),
Offset: uint32(p.subsliceOffset(b)), //nolint:gosec // TOML documents are small
Length: uint32(len(b)), //nolint:gosec // TOML documents are small
}
}
// rangeOfToken computes the Range of a token given the remaining bytes after the token.
// This is used when the token was extracted from the beginning of some position,
// and 'rest' is what remains after the token.
func (p *Parser) rangeOfToken(token, rest []byte) Range {
offset := len(p.data) - len(token) - len(rest)
return Range{Offset: uint32(offset), Length: uint32(len(token))} //nolint:gosec // TOML documents are small
}
// subsliceOffset returns the byte offset of subslice b within p.data.
// b must share the same backing array as p.data.
func (p *Parser) subsliceOffset(b []byte) int {
if len(b) == 0 {
return len(p.data)
}
dataPtr := reflect.ValueOf(p.data).Pointer()
subPtr := reflect.ValueOf(b).Pointer()
offset := int(subPtr - dataPtr)
if offset < 0 || offset > len(p.data) {
panic("subslice is not within data")
}
return offset
}
// Raw returns the slice corresponding to the bytes in the given range.
func (p *Parser) Raw(raw Range) []byte {
return p.data[raw.Offset : raw.Offset+raw.Length]
@@ -158,9 +181,17 @@ type Shape struct {
End Position
}
func (p *Parser) position(b []byte) Position {
offset := danger.SubsliceOffset(p.data, b)
// Shape returns the shape of the given range in the input. Will
// panic if the range is not a subslice of the input.
func (p *Parser) Shape(r Range) Shape {
return Shape{
Start: p.positionAt(int(r.Offset)),
End: p.positionAt(int(r.Offset + r.Length)),
}
}
// positionAt returns the position at the given byte offset in the document.
func (p *Parser) positionAt(offset int) Position {
lead := p.data[:offset]
return Position{
@@ -170,16 +201,6 @@ func (p *Parser) position(b []byte) Position {
}
}
// Shape returns the shape of the given range in the input. Will
// panic if the range is not a subslice of the input.
func (p *Parser) Shape(r Range) Shape {
raw := p.Raw(r)
return Shape{
Start: p.position(raw),
End: p.position(raw[r.Length:]),
}
}
func (p *Parser) parseNewline(b []byte) ([]byte, error) {
if b[0] == '\n' {
return b[1:], nil
@@ -199,7 +220,7 @@ func (p *Parser) parseComment(b []byte) (reference, []byte, error) {
if p.KeepComments && err == nil {
ref = p.builder.Push(Node{
Kind: Comment,
Raw: p.Range(data),
Raw: p.rangeOfToken(data, rest),
Data: data,
})
}
@@ -316,6 +337,9 @@ func (p *Parser) parseStdTable(b []byte) (reference, []byte, error) {
func (p *Parser) parseKeyval(b []byte) (reference, []byte, error) {
// keyval = key keyval-sep val
// Track the start position for Raw range
startB := b
ref := p.builder.Push(Node{
Kind: KeyValue,
})
@@ -330,7 +354,7 @@ func (p *Parser) parseKeyval(b []byte) (reference, []byte, error) {
b = p.parseWhitespace(b)
if len(b) == 0 {
return invalidReference, nil, NewParserError(b, "expected = after a key, but the document ends there")
return invalidReference, nil, NewParserError(startB[:len(startB)-len(b)], "expected = after a key, but the document ends there")
}
b, err = expect('=', b)
@@ -348,6 +372,11 @@ func (p *Parser) parseKeyval(b []byte) (reference, []byte, error) {
p.builder.Chain(valRef, key)
p.builder.AttachChild(ref, valRef)
// Set Raw to span the entire key-value expression.
// Access the node directly in the slice to avoid the write barrier
// that NodeAt's nodes-pointer setup would trigger.
p.builder.tree.nodes[ref].Raw = p.rangeOfToken(startB[:len(startB)-len(b)], b)
return ref, b, err
}
@@ -376,7 +405,7 @@ func (p *Parser) parseVal(b []byte) (reference, []byte, error) {
if err == nil {
ref = p.builder.Push(Node{
Kind: String,
Raw: p.Range(raw),
Raw: p.rangeOfToken(raw, b),
Data: v,
})
}
@@ -394,7 +423,7 @@ func (p *Parser) parseVal(b []byte) (reference, []byte, error) {
if err == nil {
ref = p.builder.Push(Node{
Kind: String,
Raw: p.Range(raw),
Raw: p.rangeOfToken(raw, b),
Data: v,
})
}
@@ -456,7 +485,7 @@ func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) {
// inline-table-keyvals = keyval [ inline-table-sep inline-table-keyvals ]
parent := p.builder.Push(Node{
Kind: InlineTable,
Raw: p.Range(b[:1]),
Raw: p.rangeOfToken(b[:1], b[1:]),
})
first := true
@@ -542,7 +571,7 @@ func (p *Parser) parseValArray(b []byte) (reference, []byte, error) {
var err error
for len(b) > 0 {
cref := invalidReference
var cref reference
cref, b, err = p.parseOptionalWhitespaceCommentNewline(b)
if err != nil {
return parent, nil, err
@@ -611,12 +640,13 @@ func (p *Parser) parseOptionalWhitespaceCommentNewline(b []byte) (reference, []b
latestCommentRef := invalidReference
addComment := func(ref reference) {
if rootCommentRef == invalidReference {
switch {
case rootCommentRef == invalidReference:
rootCommentRef = ref
} else if latestCommentRef == invalidReference {
case latestCommentRef == invalidReference:
p.builder.AttachChild(rootCommentRef, ref)
latestCommentRef = ref
} else {
default:
p.builder.Chain(latestCommentRef, ref)
latestCommentRef = ref
}
@@ -704,11 +734,11 @@ func (p *Parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, er
if !escaped {
str := token[startIdx:endIdx]
verr := characters.Utf8TomlValidAlreadyEscaped(str)
if verr.Zero() {
highlight := characters.Utf8TomlValidAlreadyEscaped(str)
if len(highlight) == 0 {
return token, str, rest, nil
}
return nil, nil, nil, NewParserError(str[verr.Index:verr.Index+verr.Size], "invalid UTF-8")
return nil, nil, nil, NewParserError(highlight, "invalid UTF-8")
}
var builder bytes.Buffer
@@ -744,7 +774,7 @@ func (p *Parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, er
i += j
for ; i < len(token)-3; i++ {
c := token[i]
if !(c == '\n' || c == '\r' || c == ' ' || c == '\t') {
if c != '\n' && c != '\r' && c != ' ' && c != '\t' {
i--
break
}
@@ -820,7 +850,7 @@ func (p *Parser) parseKey(b []byte) (reference, []byte, error) {
ref := p.builder.Push(Node{
Kind: Key,
Raw: p.Range(raw),
Raw: p.rangeOfToken(raw, b),
Data: key,
})
@@ -836,7 +866,7 @@ func (p *Parser) parseKey(b []byte) (reference, []byte, error) {
p.builder.PushAndChain(Node{
Kind: Key,
Raw: p.Range(raw),
Raw: p.rangeOfToken(raw, b),
Data: key,
})
} else {
@@ -897,11 +927,11 @@ func (p *Parser) parseBasicString(b []byte) ([]byte, []byte, []byte, error) {
// validate the string and return a direct reference to the buffer.
if !escaped {
str := token[startIdx:endIdx]
verr := characters.Utf8TomlValidAlreadyEscaped(str)
if verr.Zero() {
highlight := characters.Utf8TomlValidAlreadyEscaped(str)
if len(highlight) == 0 {
return token, str, rest, nil
}
return nil, nil, nil, NewParserError(str[verr.Index:verr.Index+verr.Size], "invalid UTF-8")
return nil, nil, nil, NewParserError(highlight, "invalid UTF-8")
}
i := startIdx
@@ -972,7 +1002,7 @@ func hexToRune(b []byte, length int) (rune, error) {
var r uint32
for i, c := range b {
d := uint32(0)
var d uint32
switch {
case '0' <= c && c <= '9':
d = uint32(c - '0')
@@ -1013,7 +1043,7 @@ func (p *Parser) parseIntOrFloatOrDateTime(b []byte) (reference, []byte, error)
return p.builder.Push(Node{
Kind: Float,
Data: b[:3],
Raw: p.Range(b[:3]),
Raw: p.rangeOfToken(b[:3], b[3:]),
}), b[3:], nil
case 'n':
if !scanFollowsNan(b) {
@@ -1023,7 +1053,7 @@ func (p *Parser) parseIntOrFloatOrDateTime(b []byte) (reference, []byte, error)
return p.builder.Push(Node{
Kind: Float,
Data: b[:3],
Raw: p.Range(b[:3]),
Raw: p.rangeOfToken(b[:3], b[3:]),
}), b[3:], nil
case '+', '-':
return p.scanIntOrFloat(b)
@@ -1076,7 +1106,7 @@ byteLoop:
}
case c == 'T' || c == 't' || c == ':' || c == '.':
hasTime = true
case c == '+' || c == '-' || c == 'Z' || c == 'z':
case c == '+' || c == 'Z' || c == 'z':
hasTz = true
case c == ' ':
if !seenSpace && i+1 < len(b) && isDigit(b[i+1]) {
@@ -1148,7 +1178,7 @@ func (p *Parser) scanIntOrFloat(b []byte) (reference, []byte, error) {
return p.builder.Push(Node{
Kind: Integer,
Data: b[:i],
Raw: p.Range(b[:i]),
Raw: p.rangeOfToken(b[:i], b[i:]),
}), b[i:], nil
}
@@ -1172,7 +1202,7 @@ func (p *Parser) scanIntOrFloat(b []byte) (reference, []byte, error) {
return p.builder.Push(Node{
Kind: Float,
Data: b[:i+3],
Raw: p.Range(b[:i+3]),
Raw: p.rangeOfToken(b[:i+3], b[i+3:]),
}), b[i+3:], nil
}
@@ -1184,7 +1214,7 @@ func (p *Parser) scanIntOrFloat(b []byte) (reference, []byte, error) {
return p.builder.Push(Node{
Kind: Float,
Data: b[:i+3],
Raw: p.Range(b[:i+3]),
Raw: p.rangeOfToken(b[:i+3], b[i+3:]),
}), b[i+3:], nil
}
@@ -1207,7 +1237,7 @@ func (p *Parser) scanIntOrFloat(b []byte) (reference, []byte, error) {
return p.builder.Push(Node{
Kind: kind,
Data: b[:i],
Raw: p.Range(b[:i]),
Raw: p.rangeOfToken(b[:i], b[i:]),
}), b[i:], nil
}
+28 -3
View File
@@ -1,7 +1,32 @@
package unstable
// The Unmarshaler interface may be implemented by types to customize their
// behavior when being unmarshaled from a TOML document.
// Unmarshaler is implemented by types that can unmarshal a TOML
// description of themselves. The input is a valid TOML document
// containing the relevant portion of the parsed document.
//
// For tables (including split tables defined in multiple places),
// the data contains the raw key-value bytes from the original document
// with adjusted table headers to be relative to the unmarshaling target.
type Unmarshaler interface {
UnmarshalTOML(value *Node) error
UnmarshalTOML(data []byte) error
}
// RawMessage is a raw encoded TOML value. It implements Unmarshaler
// and can be used to delay TOML decoding or capture raw content.
//
// Example usage:
//
// type Config struct {
// Plugin RawMessage `toml:"plugin"`
// }
//
// var cfg Config
// toml.NewDecoder(r).EnableUnmarshalerInterface().Decode(&cfg)
// // cfg.Plugin now contains the raw TOML bytes for [plugin]
type RawMessage []byte
// UnmarshalTOML implements Unmarshaler.
func (m *RawMessage) UnmarshalTOML(data []byte) error {
*m = append((*m)[0:0], data...)
return nil
}
+1
View File
@@ -0,0 +1 @@
bin/
+19 -16
View File
@@ -1,32 +1,35 @@
run:
timeout: 30m
version: "2"
linters:
default: none
enable:
- gofmt
- depguard
- govet
- goimports
- ineffassign
- misspell
- unused
- staticcheck
- typecheck
disable-all: true
linters-settings:
- unused
settings:
depguard:
rules:
main:
deny:
# The io/ioutil package has been deprecated.
# https://go.dev/doc/go1.16#ioutil
- pkg: "io/ioutil"
- pkg: io/ioutil
desc: The io/ioutil package has been deprecated.
exclusions:
generated: lax
paths:
- .*\.pb\.go$
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
paths:
- .*\.pb\.go$
issues:
exclude-files:
- ".*\\.pb\\.go$"
# show all
max-issues-per-linter: 0
max-same-issues: 0
+54 -3
View File
@@ -1,11 +1,12 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.23
ARG XX_VERSION=1.6.1
ARG GO_VERSION=1.26
ARG ALPINE_VERSION=3.23
ARG XX_VERSION=1.9.0
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS base
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
RUN apk add --no-cache git
COPY --from=xx / /
WORKDIR /src
@@ -35,4 +36,54 @@ COPY --from=test /tmp/coverage.txt /coverage-root.txt
FROM scratch AS test-noroot-coverage
COPY --from=test-noroot /tmp/coverage.txt /coverage-noroot.txt
FROM base AS bench-base
WORKDIR /app
RUN --mount=type=bind,source=go.mod,target=/app/go.mod \
--mount=type=bind,source=go.sum,target=/app/go.sum \
--mount=type=cache,target=/root/.cache \
--mount=type=cache,target=/go/pkg/mod <<EOT
set -ex
apk add --no-cache rsync
go install tool github.com/jstemmer/go-junit-report/v2
EOT
FROM bench-base AS bench
WORKDIR /src
ARG BENCH_FILE_SIZE
RUN --mount=target=. \
--mount=target=/go/pkg/mod,type=cache \
--mount=target=/root/.cache,type=cache <<EOT
set -ex
set -o pipefail
mkdir -p /tmp/bench-results
CGO_ENABLED=0 xx-go test -benchmem -bench=. -run=^$ . 2>&1 | tee /tmp/fsutil.log
go-junit-report -in /tmp/fsutil.log -out /tmp/bench-results/fsutil.junit.xml
cd bench
CGO_ENABLED=0 xx-go test -benchmem -bench=. -run=^$ . 2>&1 | tee /tmp/bench.log
go-junit-report -in /tmp/bench.log -out /tmp/bench-results/bench.junit.xml
EOT
FROM bench-base AS bench-noroot
WORKDIR /src
RUN mkdir -p /go/pkg && chmod 0777 /go/pkg
USER 1000:1000
ARG BENCH_FILE_SIZE
RUN --mount=target=. \
--mount=target=/tmp/.cache,type=cache <<EOT
set -ex
set -o pipefail
mkdir -p /tmp/bench-results
CGO_ENABLED=0 GOCACHE=/tmp/gocache xx-go test -bench=. -benchmem -run=^$ . 2>&1 | tee /tmp/fsutil.log
go-junit-report -in /tmp/fsutil.log -out /tmp/bench-results/fsutil.junit.xml
cd bench
CGO_ENABLED=0 GOCACHE=/tmp/gocache xx-go test -bench=. -benchmem -run=^$ . 2>&1 | tee /tmp/bench.log
go-junit-report -in /tmp/bench.log -out /tmp/bench-results/bench.junit.xml
EOT
FROM scratch AS bench-root-results
COPY --from=bench /tmp/bench-results /bench-root
FROM scratch AS bench-noroot-results
COPY --from=bench-noroot /tmp/bench-results /bench-noroot
FROM build
+3
View File
@@ -10,3 +10,6 @@ coverage:
github_checks:
annotations: false
ignore:
- "**/*.pb.go"
+2 -3
View File
@@ -21,7 +21,7 @@ import (
const defaultDirectoryMode = 0755
var bufferPool = &sync.Pool{
New: func() interface{} {
New: func() any {
buffer := make([]byte, 32*1024)
return &buffer
},
@@ -738,8 +738,7 @@ func rel(basepath, targpath string) (string, error) {
// filepath.Rel can't handle UUID paths in windows
if runtime.GOOS == "windows" {
pfx := basepath + `\`
if strings.HasPrefix(targpath, pfx) {
p := strings.TrimPrefix(targpath, pfx)
if p, ok := strings.CutPrefix(targpath, pfx); ok {
if p == "" {
p = "."
}
+66 -8
View File
@@ -3,7 +3,10 @@ package fsutil
import (
"context"
"hash"
gofs "io/fs"
"os"
"path/filepath"
"runtime"
"github.com/pkg/errors"
"github.com/tonistiigi/fsutil/types"
@@ -11,26 +14,41 @@ import (
type walkerFn func(ctx context.Context, pathC chan<- *currentPath) error
func Changes(ctx context.Context, a, b walkerFn, changeFn ChangeFunc) error {
return nil
}
type HandleChangeFn func(ChangeKind, string, os.FileInfo, error) error
type ContentHasher func(*types.Stat) (hash.Hash, error)
func getWalkerFn(root string) walkerFn {
return getFSWalkerFn(func() (FS, error) {
return NewFS(root)
})
}
func getRootWalkerFn(root Root) walkerFn {
return getFSWalkerFn(func() (FS, error) {
return NewRootFS(root), nil
})
}
func getFSWalkerFn(newFS func() (FS, error)) walkerFn {
return func(ctx context.Context, pathC chan<- *currentPath) error {
return errors.Wrap(Walk(ctx, root, nil, func(path string, f os.FileInfo, err error) error {
fs, err := newFS()
if err != nil {
return errors.Wrap(err, "failed to walk")
}
return errors.Wrap(fs.Walk(ctx, "/", func(path string, entry gofs.DirEntry, err error) error {
if err != nil {
return err
}
stat, ok := f.Sys().(*types.Stat)
fi, err := entry.Info()
if err != nil {
return err
}
stat, ok := fi.Sys().(*types.Stat)
if !ok {
return errors.Errorf("%T invalid file without stat information", f.Sys())
return errors.Errorf("%T invalid file without stat information", fi.Sys())
}
p := &currentPath{
path: path,
stat: stat,
@@ -46,6 +64,46 @@ func getWalkerFn(root string) walkerFn {
}
}
func mkrootstat(root Root, relpath string, fi os.FileInfo, inodemap map[uint64]string) (*types.Stat, error) {
stat := &types.Stat{
Path: filepath.FromSlash(filepath.ToSlash(relpath)),
Mode: uint32(fi.Mode()),
ModTime: fi.ModTime().UnixNano(),
}
setUnixOpt(fi, stat, relpath, inodemap)
if !fi.IsDir() {
stat.Size = fi.Size()
if fi.Mode()&os.ModeSymlink != 0 {
link, err := root.Readlink(relpath)
if err != nil {
return nil, errors.WithStack(err)
}
stat.Linkname = link
}
}
if fi.IsDir() || fi.Mode().IsRegular() {
if err := loadRootXattr(root, relpath, stat); err != nil {
return nil, err
}
}
if runtime.GOOS == "windows" {
permPart := stat.Mode & uint32(os.ModePerm)
noPermPart := stat.Mode &^ uint32(os.ModePerm)
// Add the x bit: make everything +x from windows
permPart |= 0111
permPart &= 0755
stat.Mode = noPermPart | permPart
}
// Clear the socket bit since archive/tar.FileInfoHeader does not handle it
stat.Mode &^= uint32(os.ModeSocket)
return stat, nil
}
func emptyWalker(ctx context.Context, pathC chan<- *currentPath) error {
return nil
}
+22
View File
@@ -6,6 +6,10 @@ variable "DESTDIR" {
default = "./bin"
}
variable "BENCH_FILE_SIZE" {
default = null
}
target "_platforms" {
platforms = [
"darwin/amd64",
@@ -60,6 +64,24 @@ target "test-noroot" {
output = ["${DESTDIR}/coverage"]
}
target "bench-root" {
inherits = ["build"]
target = "bench-root-results"
output = ["${DESTDIR}/bench"]
args = {
BENCH_FILE_SIZE = BENCH_FILE_SIZE
}
}
target "bench-noroot" {
inherits = ["build"]
target = "bench-noroot-results"
output = ["${DESTDIR}/bench"]
args = {
BENCH_FILE_SIZE = BENCH_FILE_SIZE
}
}
group "lint" {
targets = ["lint-golangci", "lint-gopls"]
}
+6 -6
View File
@@ -326,10 +326,10 @@ func (fs *filterFS) Walk(ctx context.Context, target string, fn gofs.WalkDirFunc
return ctx.Err()
default:
if fs.mapFn != nil {
result := fs.mapFn(stat.Path, stat)
if result == MapResultSkipDir {
switch result := fs.mapFn(stat.Path, stat); result {
case MapResultSkipDir:
return filepath.SkipDir
} else if result == MapResultExclude {
case MapResultExclude:
return nil
}
}
@@ -355,10 +355,10 @@ func (fs *filterFS) Walk(ctx context.Context, target string, fn gofs.WalkDirFunc
default:
}
if fs.mapFn != nil {
result := fs.mapFn(parentStat.Path, parentStat)
if result == MapResultExclude {
switch result := fs.mapFn(parentStat.Path, parentStat); result {
case MapResultExclude:
continue
} else if result == MapResultSkipDir {
case MapResultSkipDir:
parentDirs[i].skipFn = true
return filepath.SkipDir
}
+71 -5
View File
@@ -40,6 +40,11 @@ func NewFS(root string) (FS, error) {
}, nil
}
// NewRootFS creates a new FS from a filesystem root.
func NewRootFS(root Root) FS {
return &rootFS{root: root}
}
type fs struct {
root string
}
@@ -90,6 +95,67 @@ func (fs *fs) Open(p string) (io.ReadCloser, error) {
return rc, errors.WithStack(err)
}
type rootFS struct {
root Root
}
func (fs *rootFS) Walk(ctx context.Context, target string, fn gofs.WalkDirFunc) error {
seenFiles := make(map[uint64]string)
target = cleanRootFSTarget(target)
return gofs.WalkDir(fs.root.FS(), target, func(path string, dirEntry gofs.DirEntry, walkErr error) (retErr error) {
defer func() {
if retErr != nil && isNotExist(retErr) {
retErr = filepath.SkipDir
}
}()
path = filepath.FromSlash(path)
if path == "." {
return nil
}
var entry gofs.DirEntry
if dirEntry != nil {
fi, err := fs.root.Lstat(path)
if err != nil {
return errors.WithStack(err)
}
stat, err := mkrootstat(fs.root, path, fi, seenFiles)
if err != nil {
return err
}
entry = &DirEntryInfo{Stat: stat}
}
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := fn(path, entry, walkErr); err != nil {
return err
}
}
return nil
})
}
func (fs *rootFS) Open(p string) (io.ReadCloser, error) {
rc, err := fs.root.OpenFile(cleanRootPath(p), os.O_RDONLY, 0)
return rc, errors.WithStack(err)
}
func cleanRootFSTarget(target string) string {
target = cleanRootPath(target)
for strings.HasPrefix(target, string(filepath.Separator)) {
target = strings.TrimPrefix(target, string(filepath.Separator))
}
if target == "" || target == "." {
return "."
}
return filepath.ToSlash(target)
}
type Dir struct {
Stat *types.Stat
FS FS
@@ -187,7 +253,7 @@ type StatInfo struct {
}
func (s *StatInfo) Name() string {
return filepath.Base(s.Stat.Path)
return filepath.Base(s.Path)
}
func (s *StatInfo) Size() int64 {
@@ -206,7 +272,7 @@ func (s *StatInfo) IsDir() bool {
return s.Mode().IsDir()
}
func (s *StatInfo) Sys() interface{} {
func (s *StatInfo) Sys() any {
return s.Stat
}
@@ -221,7 +287,7 @@ type DirEntryInfo struct {
func (s *DirEntryInfo) Name() string {
if s.Stat != nil {
return filepath.Base(s.Stat.Path)
return filepath.Base(s.Path)
}
return s.entry.Name()
}
@@ -235,7 +301,7 @@ func (s *DirEntryInfo) IsDir() bool {
func (s *DirEntryInfo) Type() gofs.FileMode {
if s.Stat != nil {
return gofs.FileMode(s.Stat.Mode)
return gofs.FileMode(s.Mode)
}
return s.entry.Type()
}
@@ -253,6 +319,6 @@ func (s *DirEntryInfo) Info() (gofs.FileInfo, error) {
s.Stat = stat
}
st := s.Stat.Clone()
st := s.Clone()
return &StatInfo{st}, nil
}
+60 -41
View File
@@ -6,45 +6,64 @@
Incremental file directory sync tools in golang.
```
BENCH_FILE_SIZE=10000 ./bench.test --test.bench .
BenchmarkCopyWithTar10-4 2000 995242 ns/op
BenchmarkCopyWithTar50-4 300 4710021 ns/op
BenchmarkCopyWithTar200-4 100 16627260 ns/op
BenchmarkCopyWithTar1000-4 20 60031459 ns/op
BenchmarkCPA10-4 1000 1678367 ns/op
BenchmarkCPA50-4 500 3690306 ns/op
BenchmarkCPA200-4 200 9495066 ns/op
BenchmarkCPA1000-4 50 29769289 ns/op
BenchmarkDiffCopy10-4 2000 943889 ns/op
BenchmarkDiffCopy50-4 500 3285950 ns/op
BenchmarkDiffCopy200-4 200 8563792 ns/op
BenchmarkDiffCopy1000-4 50 29511340 ns/op
BenchmarkDiffCopyProto10-4 2000 944615 ns/op
BenchmarkDiffCopyProto50-4 500 3334940 ns/op
BenchmarkDiffCopyProto200-4 200 9420038 ns/op
BenchmarkDiffCopyProto1000-4 50 30632429 ns/op
BenchmarkIncrementalDiffCopy10-4 2000 691993 ns/op
BenchmarkIncrementalDiffCopy50-4 1000 1304253 ns/op
BenchmarkIncrementalDiffCopy200-4 500 3306519 ns/op
BenchmarkIncrementalDiffCopy1000-4 200 10211343 ns/op
BenchmarkIncrementalDiffCopy5000-4 20 55194427 ns/op
BenchmarkIncrementalDiffCopy10000-4 20 91759289 ns/op
BenchmarkIncrementalCopyWithTar10-4 2000 1020258 ns/op
BenchmarkIncrementalCopyWithTar50-4 300 5348786 ns/op
BenchmarkIncrementalCopyWithTar200-4 100 19495000 ns/op
BenchmarkIncrementalCopyWithTar1000-4 20 70338507 ns/op
BenchmarkIncrementalRsync10-4 30 45215754 ns/op
BenchmarkIncrementalRsync50-4 30 45837260 ns/op
BenchmarkIncrementalRsync200-4 30 48780614 ns/op
BenchmarkIncrementalRsync1000-4 20 54801892 ns/op
BenchmarkIncrementalRsync5000-4 20 84782542 ns/op
BenchmarkIncrementalRsync10000-4 10 103355108 ns/op
BenchmarkRsync10-4 30 46776470 ns/op
BenchmarkRsync50-4 30 48601555 ns/op
BenchmarkRsync200-4 20 59642691 ns/op
BenchmarkRsync1000-4 20 101343010 ns/op
BenchmarkGnuTar10-4 500 3171448 ns/op
BenchmarkGnuTar50-4 300 5030296 ns/op
BenchmarkGnuTar200-4 100 10464313 ns/op
BenchmarkGnuTar1000-4 50 30375257 ns/op
BENCH_FILE_SIZE=10000 docker buildx bake bench-root
...
#17 0.303 + CGO_ENABLED=0 xx-go test -benchmem '-bench=.' '-run=^$' .
#17 0.303 + tee /tmp/fsutil.log
#17 1.527 BenchmarkWalker/depth_1_target-32 28356 42258 ns/op 9234 B/op 174 allocs/op
#17 3.166 BenchmarkWalker/depth_1_doublestar_target-32 28647 42038 ns/op 9282 B/op 175 allocs/op
#17 4.865 BenchmarkWalker/depth_2_star_target-32 1184 1009371 ns/op 200654 B/op 3971 allocs/op
#17 6.342 BenchmarkWalker/depth_2_doublestar_target-32 1148 1007115 ns/op 195891 B/op 3908 allocs/op
#17 9.339 BenchmarkWalker/depth_3_star_star_target-32 39 28146516 ns/op 5221363 B/op 100915 allocs/op
#17 13.54 BenchmarkWalker/depth_3_doublestar_target-32 40 28496829 ns/op 5206464 B/op 100828 allocs/op
#17 17.99 BenchmarkWalker/depth_4_star_star_star_target-32 26 48224854 ns/op 6571213 B/op 119421 allocs/op
#17 25.32 BenchmarkWalker/depth_4_doublestar_target-32 24 45061931 ns/op 6488522 B/op 119315 allocs/op
#17 28.67 BenchmarkWalker/depth_5_star_star_star_star_target-32 54 22124864 ns/op 2476377 B/op 42818 allocs/op
#17 32.59 BenchmarkWalker/depth_5_doublestar_target-32 49 21479412 ns/op 2460690 B/op 42699 allocs/op
#17 35.09 BenchmarkWalker/depth_6_star_star_star_star_star_target-32 28 38307776 ns/op 3998884 B/op 67772 allocs/op
#17 38.05 BenchmarkWalker/depth_6_doublestar_target-32 31 38242074 ns/op 3980841 B/op 67634 allocs/op
#17 42.92 BenchmarkWalker/depth_6_doublestar_exclude_star_star_doublestar-32 2925 393602 ns/op 47439 B/op 1006 allocs/op
#17 45.99 + cd bench
#17 45.99 + CGO_ENABLED=0 xx-go test -benchmem '-bench=.' '-run=^$' .
#17 45.99 + tee /tmp/bench.log
#17 46.84 BenchmarkCopyWithTar10-32 283 4291776 ns/op 906824 B/op 843 allocs/op
#17 50.05 BenchmarkCopyWithTar50-32 50 24077499 ns/op 4999874 B/op 4514 allocs/op
#17 54.00 BenchmarkCopyWithTar200-32 15 74350687 ns/op 18757006 B/op 15347 allocs/op
#17 56.31 BenchmarkCopyWithTar1000-32 5 259188166 ns/op 72393427 B/op 55045 allocs/op
#17 61.31 BenchmarkCPA10-32 339 3496169 ns/op 7102 B/op 77 allocs/op
#17 64.68 BenchmarkCPA50-32 74 15808000 ns/op 7102 B/op 77 allocs/op
#17 67.26 BenchmarkCPA200-32 26 45892546 ns/op 7101 B/op 77 allocs/op
#17 70.08 BenchmarkCPA1000-32 8 147602854 ns/op 7103 B/op 77 allocs/op
#17 72.60 BenchmarkDiffCopy10-32 392 3045072 ns/op 219789 B/op 1123 allocs/op
#17 76.15 BenchmarkDiffCopy50-32 82 14024073 ns/op 1253831 B/op 5460 allocs/op
#17 78.83 BenchmarkDiffCopy200-32 27 41259003 ns/op 4683591 B/op 18640 allocs/op
#17 81.60 BenchmarkDiffCopy1000-32 8 125244042 ns/op 17285147 B/op 67649 allocs/op
#17 83.90 BenchmarkDiffCopyProto10-32 412 2934103 ns/op 232184 B/op 1143 allocs/op
#17 87.46 BenchmarkDiffCopyProto50-32 80 13809955 ns/op 1273311 B/op 5565 allocs/op
#17 90.02 BenchmarkDiffCopyProto200-32 30 41169665 ns/op 4697476 B/op 18995 allocs/op
#17 93.05 BenchmarkDiffCopyProto1000-32 8 127126319 ns/op 17334920 B/op 68883 allocs/op
#17 95.37 BenchmarkIncrementalDiffCopy10-32 1540 779568 ns/op 119068 B/op 1015 allocs/op
#17 97.84 BenchmarkIncrementalDiffCopy50-32 782 1513121 ns/op 455740 B/op 4285 allocs/op
#17 99.97 BenchmarkIncrementalDiffCopy200-32 271 4248983 ns/op 1329875 B/op 13603 allocs/op
#17 102.3 BenchmarkIncrementalDiffCopy1000-32 84 14027390 ns/op 4552790 B/op 46849 allocs/op
#17 105.4 BenchmarkIncrementalDiffCopy5000-32 14 73269136 ns/op 24500669 B/op 266772 allocs/op
#17 110.6 BenchmarkIncrementalDiffCopy10000-32 9 128400623 ns/op 43706410 B/op 472982 allocs/op
#17 114.4 BenchmarkIncrementalCopyWithTar10-32 396 2946217 ns/op 915096 B/op 826 allocs/op
#17 116.3 BenchmarkIncrementalCopyWithTar50-32 69 16767967 ns/op 5093048 B/op 4472 allocs/op
#17 117.7 BenchmarkIncrementalCopyWithTar200-32 19 62520307 ns/op 19081658 B/op 15270 allocs/op
#17 119.7 BenchmarkIncrementalCopyWithTar1000-32 5 239712851 ns/op 73113704 B/op 54938 allocs/op
#17 122.8 BenchmarkIncrementalRsync10-32 26 43773727 ns/op 6608 B/op 69 allocs/op
#17 124.1 BenchmarkIncrementalRsync50-32 25 45985011 ns/op 6608 B/op 69 allocs/op
#17 125.6 BenchmarkIncrementalRsync200-32 22 49976819 ns/op 6608 B/op 69 allocs/op
#17 127.3 BenchmarkIncrementalRsync1000-32 19 63618139 ns/op 6600 B/op 69 allocs/op
#17 130.6 BenchmarkIncrementalRsync5000-32 8 132002745 ns/op 6608 B/op 69 allocs/op
#17 136.3 BenchmarkIncrementalRsync10000-32 6 187247351 ns/op 6608 B/op 69 allocs/op
#17 140.3 BenchmarkRsync10-32 25 46054741 ns/op 6606 B/op 69 allocs/op
#17 141.6 BenchmarkRsync50-32 19 58922835 ns/op 6605 B/op 69 allocs/op
#17 143.2 BenchmarkRsync200-32 13 91176938 ns/op 6606 B/op 69 allocs/op
#17 145.5 BenchmarkRsync1000-32 6 198319527 ns/op 6606 B/op 69 allocs/op
#17 147.6 BenchmarkGnuTar10-32 268 4489528 ns/op 14192 B/op 151 allocs/op
#17 150.8 BenchmarkGnuTar50-32 54 20528041 ns/op 14192 B/op 151 allocs/op
#17 152.9 BenchmarkGnuTar200-32 19 60394926 ns/op 14192 B/op 151 allocs/op
#17 155.4 BenchmarkGnuTar1000-32 6 198630328 ns/op 14192 B/op 151 allocs/op
```
+67 -6
View File
@@ -64,13 +64,32 @@ type ReceiveOpt struct {
MetadataOnly FilterFunc
}
type receiveDiskWriter interface {
HandleChange(ChangeKind, string, os.FileInfo, error) error
Wait(context.Context) error
}
func Receive(ctx context.Context, conn Stream, dest string, opt ReceiveOpt) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
r := &receiver{
r := newReceiver(conn, opt)
r.dest = dest
return r.run(ctx)
}
func ReceiveRoot(ctx context.Context, conn Stream, dest Root, opt ReceiveOpt) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
r := newReceiver(conn, opt)
r.root = dest
return r.run(ctx)
}
func newReceiver(conn Stream, opt ReceiveOpt) *receiver {
return &receiver{
conn: &syncStream{Stream: conn},
dest: dest,
files: make(map[string]uint32),
pipes: make(map[uint32]io.WriteCloser),
notifyHashed: opt.NotifyHashed,
@@ -81,11 +100,11 @@ func Receive(ctx context.Context, conn Stream, dest string, opt ReceiveOpt) erro
differ: opt.Differ,
metadataOnly: opt.MetadataOnly,
}
return r.run(ctx)
}
type receiver struct {
dest string
root Root
conn Stream
files map[string]uint32
pipes map[uint32]io.WriteCloser
@@ -159,7 +178,7 @@ func (w *dynamicWalker) fill(ctx context.Context, pathC chan<- *currentPath) err
func (r *receiver) run(ctx context.Context) error {
g, ctx := errgroup.WithContext(ctx)
dw, err := NewDiskWriter(ctx, r.dest, DiskWriterOpt{
dw, err := r.newDiskWriter(ctx, DiskWriterOpt{
AsyncDataCb: r.asyncDataFunc,
NotifyCb: r.notifyHashed,
ContentHasher: r.contentHasher,
@@ -179,12 +198,21 @@ func (r *receiver) run(ctx context.Context) error {
g.Go(func() (retErr error) {
defer func() {
if retErr != nil {
// If we're unwinding because the errgroup context was
// cancelled by another goroutine's failure, report that root
// cause instead of the bare "context canceled", which would
// otherwise overwrite the real error on the sender side.
if errors.Is(retErr, context.Canceled) {
if cause := context.Cause(ctx); cause != nil {
retErr = cause
}
}
r.conn.SendMsg(&types.Packet{Type: types.PACKET_ERR, Data: []byte(retErr.Error())})
}
}()
destWalker := emptyWalker
if !r.merge {
destWalker = getWalkerFn(r.dest)
destWalker = r.destWalker()
}
err := doubleWalkDiff(ctx, dw.HandleChange, destWalker, w.fill, r.filter, r.differ)
if err != nil {
@@ -333,12 +361,45 @@ func (r *receiver) run(ctx context.Context) error {
return nil
}
return r.writeMetadata(metadataBuffer)
}
func (r *receiver) newDiskWriter(ctx context.Context, opt DiskWriterOpt) (receiveDiskWriter, error) {
if r.root != nil {
return NewRootDiskWriter(ctx, r.root, opt)
}
return NewDiskWriter(ctx, r.dest, opt)
}
func (r *receiver) destWalker() walkerFn {
if r.root != nil {
return getRootWalkerFn(r.root)
}
return getWalkerFn(r.dest)
}
func (r *receiver) writeMetadata(metadataBuffer *buffer) error {
if r.root != nil {
// although we don't allow tranferring metadataPath, make sure there was no preexisting file/symlink
_ = r.root.Remove(metadataPath)
f, err := r.root.OpenFile(metadataPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return errors.WithStack(err)
}
if _, err := metadataBuffer.WriteTo(f); err != nil {
f.Close()
return err
}
return f.Close()
}
// although we don't allow tranferring metadataPath, make sure there was no preexisting file/symlink
os.Remove(filepath.Join(r.dest, metadataPath))
f, err := os.OpenFile(filepath.Join(r.dest, metadataPath), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
return errors.WithStack(err)
}
if _, err := metadataBuffer.WriteTo(f); err != nil {
f.Close()
+85
View File
@@ -0,0 +1,85 @@
package fsutil
import (
gofs "io/fs"
"os"
"sync"
"time"
)
type Root interface {
Close() error
FS() gofs.FS
Remove(string) error
RemoveAll(string) error
Lstat(string) (os.FileInfo, error)
Stat(string) (os.FileInfo, error)
Mkdir(string, os.FileMode) error
Symlink(string, string) error
Link(string, string) error
OpenRoot(string) (*os.Root, error)
OpenFile(string, int, os.FileMode) (*os.File, error)
Readlink(string) (string, error)
Rename(string, string) error
Lchown(string, int, int) error
Chmod(string, os.FileMode) error
Chtimes(string, time.Time, time.Time) error
RootXattr
RootMknod
RootLChtimes
}
type RootXattr interface {
LSetxattr(name, key string, value []byte, flags int) error
}
type RootMknod interface {
Mknod(name string, mode uint32, dev int) error
}
type RootLChtimes interface {
LChtimes(name string, mtime time.Time) error
}
type root struct {
*os.Root
mu sync.Mutex
closed bool
rootDirState
}
func NewRoot(osroot *os.Root) Root {
return &root{Root: osroot}
}
func (r *root) Close() error {
if r == nil {
return nil
}
r.mu.Lock()
if r.closed {
r.mu.Unlock()
return nil
}
r.closed = true
rootDir := r.rootDir
r.rootDir = nil
osroot := r.Root
r.Root = nil
r.mu.Unlock()
var err error
if rootDir != nil {
err = rootDir.Close()
}
if osroot != nil {
if err2 := osroot.Close(); err == nil {
err = err2
}
}
return err
}
var _ Root = (*root)(nil)
+14
View File
@@ -0,0 +1,14 @@
//go:build linux || darwin || freebsd || netbsd || openbsd || dragonfly
package fsutil
import (
"os"
"sync"
)
type rootDirState struct {
rootDirOnce sync.Once
rootDir *os.File
rootDirErr error
}
+9
View File
@@ -0,0 +1,9 @@
//go:build !linux && !darwin && !freebsd && !netbsd && !openbsd && !dragonfly
package fsutil
import "os"
type rootDirState struct {
rootDir *os.File
}
+13
View File
@@ -0,0 +1,13 @@
//go:build !linux && !freebsd && !netbsd
package fsutil
import (
"os"
"github.com/pkg/errors"
)
func unsupportedRootOp(op, name string, err error) error {
return errors.WithStack(&os.PathError{Op: op, Path: name, Err: err})
}
+14
View File
@@ -0,0 +1,14 @@
//go:build !linux && !darwin && !freebsd && !netbsd && !openbsd && !dragonfly && !windows
package fsutil
import (
"syscall"
"time"
)
var _ RootLChtimes = (*root)(nil)
func (r *root) LChtimes(name string, mtime time.Time) error {
return unsupportedRootOp("utimensat", name, syscall.ENOSYS)
}
+26
View File
@@ -0,0 +1,26 @@
//go:build windows
package fsutil
import (
"os"
"time"
"github.com/pkg/errors"
)
var _ RootLChtimes = (*root)(nil)
func (r *root) LChtimes(name string, mtime time.Time) error {
fi, err := r.Lstat(name)
if err != nil {
return errors.WithStack(err)
}
if fi.Mode()&os.ModeSymlink != 0 {
return nil
}
if err := r.Chtimes(name, mtime, mtime); err != nil {
return errors.WithStack(err)
}
return nil
}
+27
View File
@@ -0,0 +1,27 @@
//go:build freebsd
package fsutil
import (
"os"
"github.com/pkg/errors"
"golang.org/x/sys/unix"
)
var _ RootMknod = (*root)(nil)
func (r *root) Mknod(name string, mode uint32, dev int) error {
parent, base, closeParent, err := r.openRootParent(name)
if err != nil {
return err
}
if closeParent {
defer parent.Close()
}
if err := unix.Mknodat(int(parent.Fd()), base, mode, uint64(dev)); err != nil {
return errors.WithStack(&os.PathError{Op: "mknodat", Path: name, Err: err})
}
return nil
}
+27
View File
@@ -0,0 +1,27 @@
//go:build linux || netbsd || openbsd || dragonfly
package fsutil
import (
"os"
"github.com/pkg/errors"
"golang.org/x/sys/unix"
)
var _ RootMknod = (*root)(nil)
func (r *root) Mknod(name string, mode uint32, dev int) error {
parent, base, closeParent, err := r.openRootParent(name)
if err != nil {
return err
}
if closeParent {
defer parent.Close()
}
if err := unix.Mknodat(int(parent.Fd()), base, mode, dev); err != nil {
return errors.WithStack(&os.PathError{Op: "mknodat", Path: name, Err: err})
}
return nil
}
+11
View File
@@ -0,0 +1,11 @@
//go:build !linux && !freebsd && !netbsd && !openbsd && !dragonfly
package fsutil
import "syscall"
var _ RootMknod = (*root)(nil)
func (r *root) Mknod(name string, mode uint32, dev int) error {
return unsupportedRootOp("mknodat", name, syscall.ENOSYS)
}
+90
View File
@@ -0,0 +1,90 @@
//go:build linux || darwin || freebsd || netbsd || openbsd || dragonfly
package fsutil
import (
"os"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/pkg/errors"
"golang.org/x/sys/unix"
)
var _ RootLChtimes = (*root)(nil)
func (r *root) LChtimes(name string, mtime time.Time) error {
parent, base, closeParent, err := r.openRootParent(name)
if err != nil {
return err
}
if closeParent {
defer parent.Close()
}
ts := unix.NsecToTimespec(mtime.UnixNano())
times := []unix.Timespec{ts, ts}
if err := unix.UtimesNanoAt(int(parent.Fd()), base, times, unix.AT_SYMLINK_NOFOLLOW); err != nil {
return errors.WithStack(&os.PathError{Op: "utimensat", Path: name, Err: err})
}
return nil
}
func (r *root) openRootParent(name string) (*os.File, string, bool, error) {
if r == nil {
return nil, "", false, errors.New("nil root")
}
// fast path for direct basename
if !strings.ContainsRune(name, filepath.Separator) {
if name == "" || name == "." || name == ".." {
return nil, "", false, errors.WithStack(&os.PathError{Op: "openat", Path: name, Err: syscall.EINVAL})
}
parent, err := r.rootDirFile()
if err != nil {
return nil, "", false, errors.WithStack(err)
}
return parent, name, false, nil
}
cleaned := filepath.Clean(name)
base := filepath.Base(cleaned)
if base == "." || base == ".." {
return nil, "", false, errors.WithStack(&os.PathError{Op: "openat", Path: name, Err: syscall.EINVAL})
}
dir := filepath.Dir(cleaned)
if dir == "." {
parent, err := r.rootDirFile()
if err != nil {
return nil, "", false, errors.WithStack(err)
}
return parent, base, false, nil
}
parent, err := r.OpenFile(dir, os.O_RDONLY|unix.O_DIRECTORY, 0)
if err != nil {
return nil, "", false, errors.WithStack(err)
}
return parent, base, true, nil
}
func (r *root) rootDirFile() (*os.File, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.closed {
return nil, os.ErrClosed
}
if r.Root == nil {
return nil, errors.New("nil root")
}
r.rootDirOnce.Do(func() {
r.rootDir, r.rootDirErr = r.OpenFile(".", os.O_RDONLY|unix.O_DIRECTORY, 0)
})
if r.rootDirErr != nil {
return nil, r.rootDirErr
}
return r.rootDir, nil
}
+23
View File
@@ -0,0 +1,23 @@
//go:build linux || freebsd || netbsd || openbsd || dragonfly
package fsutil
import (
"os"
"syscall"
"github.com/tonistiigi/fsutil/types"
)
func handleRootTarTypeBlockCharFifo(root RootMknod, path string, stat *types.Stat) error {
mode := uint32(stat.Mode & 07777)
if os.FileMode(stat.Mode)&os.ModeCharDevice != 0 {
mode |= syscall.S_IFCHR
} else if os.FileMode(stat.Mode)&os.ModeNamedPipe != 0 {
mode |= syscall.S_IFIFO
} else {
mode |= syscall.S_IFBLK
}
return root.Mknod(path, mode, int(mkdev(stat.Devmajor, stat.Devminor)))
}
+12
View File
@@ -0,0 +1,12 @@
//go:build !linux && !freebsd && !netbsd && !openbsd && !dragonfly
package fsutil
import (
"github.com/pkg/errors"
"github.com/tonistiigi/fsutil/types"
)
func handleRootTarTypeBlockCharFifo(RootMknod, string, *types.Stat) error {
return errors.New("not implemented")
}
+25
View File
@@ -0,0 +1,25 @@
//go:build linux || darwin || freebsd || netbsd
package fsutil
import (
"os"
"github.com/pkg/errors"
"golang.org/x/sys/unix"
)
var _ RootXattr = (*root)(nil)
func (r *root) LSetxattr(name, key string, value []byte, flags int) error {
f, err := r.OpenFile(name, os.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0)
if err != nil {
return errors.WithStack(err)
}
defer f.Close()
if err := unix.Fsetxattr(int(f.Fd()), key, value, flags); err != nil {
return errors.WithStack(&os.PathError{Op: "fsetxattr", Path: name, Err: err})
}
return nil
}
+98
View File
@@ -0,0 +1,98 @@
//go:build linux || darwin || freebsd || netbsd
package fsutil
import (
"os"
"syscall"
"github.com/pkg/errors"
"github.com/tonistiigi/fsutil/types"
"golang.org/x/sys/unix"
)
const rootXattrBufferSize = 128
func loadRootXattr(root Root, path string, stat *types.Stat) error {
f, err := root.OpenFile(path, os.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0)
if err != nil {
if ignoreRootXattrOpenError(err) {
return nil
}
return errors.WithStack(err)
}
defer f.Close()
xattrs, err := rootListxattr(int(f.Fd()))
if err != nil {
if errors.Is(err, syscall.ENOTSUP) || errors.Is(err, unix.EOPNOTSUPP) || errors.Is(err, unix.ENOSYS) {
return nil
}
return errors.Wrapf(err, "failed to xattr %s", path)
}
if len(xattrs) == 0 {
return nil
}
m := make(map[string][]byte)
for _, key := range xattrs {
if skipXattr(key) {
continue
}
if v, err := rootGetxattr(int(f.Fd()), key); err == nil {
m[key] = v
}
}
if len(m) > 0 {
stat.Xattrs = m
}
return nil
}
func ignoreRootXattrOpenError(err error) bool {
return errors.Is(err, syscall.ELOOP) ||
errors.Is(err, syscall.EACCES) ||
errors.Is(err, syscall.EPERM) ||
errors.Is(err, syscall.ENXIO)
}
func rootListxattr(fd int) ([]string, error) {
return rootListxattrWith(fd, rootFlistxattr, rootParseListxattr)
}
func rootGetxattr(fd int, key string) ([]byte, error) {
buf := make([]byte, rootXattrBufferSize)
n, err := unix.Fgetxattr(fd, key, buf)
for err == unix.ERANGE {
n, err = unix.Fgetxattr(fd, key, nil)
if err != nil {
return nil, err
}
buf = make([]byte, n)
n, err = unix.Fgetxattr(fd, key, buf)
}
if err != nil {
return nil, err
}
return buf[:n], nil
}
type rootListxattrFunc func(int, []byte) (int, error)
type rootParseListxattrFunc func([]byte) []string
func rootListxattrWith(fd int, list rootListxattrFunc, parse rootParseListxattrFunc) ([]string, error) {
buf := make([]byte, rootXattrBufferSize)
n, err := list(fd, buf)
for err == unix.ERANGE {
n, err = list(fd, nil)
if err != nil {
return nil, err
}
buf = make([]byte, n)
n, err = list(fd, buf)
}
if err != nil {
return nil, err
}
return parse(buf[:n]), nil
}
+24
View File
@@ -0,0 +1,24 @@
//go:build freebsd || netbsd
package fsutil
import "golang.org/x/sys/unix"
func rootFlistxattr(fd int, buf []byte) (int, error) {
return unix.FlistxattrNS(fd, unix.EXTATTR_NAMESPACE_USER, buf)
}
func rootParseListxattr(buf []byte) []string {
var xattrs []string
for i := 0; i < len(buf); {
next := i + 1 + int(buf[i])
if next > len(buf) {
break
}
if next > i+1 {
xattrs = append(xattrs, "user."+string(buf[i+1:next]))
}
i = next
}
return xattrs
}
+24
View File
@@ -0,0 +1,24 @@
//go:build linux || darwin
package fsutil
import (
"bytes"
"golang.org/x/sys/unix"
)
func rootFlistxattr(fd int, buf []byte) (int, error) {
return unix.Flistxattr(fd, buf)
}
func rootParseListxattr(buf []byte) []string {
parts := bytes.Split(bytes.TrimSuffix(buf, []byte{0}), []byte{0})
var xattrs []string
for _, part := range parts {
if len(part) > 0 {
xattrs = append(xattrs, string(part))
}
}
return xattrs
}
+9
View File
@@ -0,0 +1,9 @@
//go:build !linux && !darwin && !freebsd && !netbsd
package fsutil
import "github.com/tonistiigi/fsutil/types"
func loadRootXattr(Root, string, *types.Stat) error {
return nil
}
+11
View File
@@ -0,0 +1,11 @@
//go:build !linux && !darwin && !freebsd && !netbsd
package fsutil
import "syscall"
var _ RootXattr = (*root)(nil)
func (r *root) LSetxattr(name, key string, value []byte, flags int) error {
return unsupportedRootOp("lsetxattr", name, syscall.ENOSYS)
}
+195
View File
@@ -0,0 +1,195 @@
package fsutil
import (
"container/list"
"os"
"path/filepath"
"strings"
"sync"
"github.com/pkg/errors"
)
const rootCacheDefaultSize = 128
type rootCache struct {
mu sync.Mutex
maxSize int
entries map[string]*rootCacheEntry
lru *list.List
closed bool
}
type rootCacheEntry struct {
path string
root Root
refs int
evicted bool
elem *list.Element
closeErr error
}
type rootLease struct {
root Root
base string
entry *rootCacheEntry
cache *rootCache
release sync.Once
}
func newRootCache(root Root, maxSize int) *rootCache {
if maxSize <= 0 {
maxSize = rootCacheDefaultSize
}
return &rootCache{
maxSize: maxSize,
entries: map[string]*rootCacheEntry{
".": {path: ".", root: root},
},
lru: list.New(),
}
}
func (c *rootCache) get(path string) (*rootLease, error) {
path = cleanRootPath(path)
dir := filepath.Dir(path)
base := filepath.Base(path)
c.mu.Lock()
defer c.mu.Unlock()
if c.closed {
return nil, errors.WithStack(os.ErrClosed)
}
entry, err := c.getDirLocked(dir)
if err != nil {
return nil, err
}
entry.refs++
return &rootLease{
root: entry.root,
base: base,
entry: entry,
cache: c,
}, nil
}
func (c *rootCache) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
c.closed = true
var err error
for path, entry := range c.entries {
if path == "." {
continue
}
delete(c.entries, path)
if entry.elem != nil {
c.lru.Remove(entry.elem)
entry.elem = nil
}
entry.evicted = true
if entry.refs == 0 {
if err2 := closeRootCacheEntry(entry); err == nil {
err = err2
}
}
}
return err
}
func (l *rootLease) Release() error {
var err error
l.release.Do(func() {
l.cache.mu.Lock()
defer l.cache.mu.Unlock()
l.entry.refs--
if l.entry.refs == 0 && l.entry.evicted {
err = closeRootCacheEntry(l.entry)
}
})
return err
}
func (c *rootCache) getDirLocked(dir string) (*rootCacheEntry, error) {
entry := c.entries["."]
if dir == "." {
return entry, nil
}
path := "."
for _, component := range rootCacheComponents(dir) {
nextPath := component
if path != "." {
nextPath = filepath.Join(path, component)
}
nextEntry, ok := c.entries[nextPath]
if ok && !nextEntry.evicted {
c.touchLocked(nextEntry)
entry = nextEntry
path = nextPath
continue
}
osroot, err := entry.root.OpenRoot(component)
if err != nil {
return nil, errors.WithStack(err)
}
nextEntry = &rootCacheEntry{
path: nextPath,
root: NewRoot(osroot),
}
nextEntry.elem = c.lru.PushFront(nextEntry)
c.entries[nextPath] = nextEntry
entry = nextEntry
path = nextPath
c.evictLocked()
}
return entry, nil
}
func (c *rootCache) touchLocked(entry *rootCacheEntry) {
if entry.elem != nil {
c.lru.MoveToFront(entry.elem)
}
}
func (c *rootCache) evictLocked() {
for len(c.entries)-1 > c.maxSize {
elem := c.lru.Back()
if elem == nil {
return
}
entry := elem.Value.(*rootCacheEntry)
c.lru.Remove(elem)
entry.elem = nil
entry.evicted = true
delete(c.entries, entry.path)
if entry.refs == 0 {
entry.closeErr = closeRootCacheEntry(entry)
}
}
}
func closeRootCacheEntry(entry *rootCacheEntry) error {
if entry.closeErr != nil {
return entry.closeErr
}
if err := entry.root.Close(); err != nil {
entry.closeErr = errors.WithStack(err)
}
return entry.closeErr
}
func rootCacheComponents(dir string) []string {
if dir == "." {
return nil
}
return strings.Split(dir, string(filepath.Separator))
}
+339
View File
@@ -0,0 +1,339 @@
package fsutil
import (
"context"
"io"
gofs "io/fs"
"os"
"path/filepath"
"syscall"
"time"
"github.com/pkg/errors"
"github.com/tonistiigi/fsutil/types"
"golang.org/x/sync/errgroup"
)
type RootDiskWriter struct {
opt DiskWriterOpt
dest Root
rootStack *rootStack
rootCache *rootCache
ctx context.Context
cancel func()
eg *errgroup.Group
egCtx context.Context
filter FilterFunc
dirModTimes map[string]int64
}
func NewRootDiskWriter(ctx context.Context, dest Root, opt DiskWriterOpt) (*RootDiskWriter, error) {
if opt.SyncDataCb == nil && opt.AsyncDataCb == nil {
return nil, errors.New("no data callback specified")
}
if opt.SyncDataCb != nil && opt.AsyncDataCb != nil {
return nil, errors.New("can't specify both sync and async data callbacks")
}
ctx, cancel := context.WithCancel(ctx)
eg, egCtx := errgroup.WithContext(ctx)
return &RootDiskWriter{
opt: opt,
dest: dest,
rootStack: newRootStack(dest),
rootCache: newRootCache(dest, rootCacheDefaultSize),
eg: eg,
ctx: ctx,
egCtx: egCtx,
cancel: cancel,
filter: opt.Filter,
dirModTimes: map[string]int64{},
}, nil
}
func (dw *RootDiskWriter) Wait(ctx context.Context) error {
err := dw.eg.Wait()
if closeErr := dw.rootCache.Close(); err == nil {
err = closeErr
}
if closeErr := dw.rootStack.Close(); err == nil {
err = closeErr
}
if err != nil {
return err
}
return gofs.WalkDir(dw.dest.FS(), ".", func(path string, d gofs.DirEntry, prevErr error) error {
if prevErr != nil {
return prevErr
}
if !d.IsDir() {
return nil
}
if mtime, ok := dw.dirModTimes[path]; ok {
return rootChtimes(dw.dest, filepath.FromSlash(path), mtime)
}
return nil
})
}
func (dw *RootDiskWriter) HandleChange(kind ChangeKind, p string, fi os.FileInfo, err error) (retErr error) {
if err != nil {
return err
}
select {
case <-dw.ctx.Done():
return dw.ctx.Err()
default:
}
defer func() {
if retErr != nil {
dw.cancel()
}
}()
destPath := cleanRootPath(p)
destRoot, base, err := dw.rootStack.get(destPath)
if err != nil {
return err
}
if kind == ChangeKindDelete {
if dw.filter != nil {
var empty types.Stat
if ok := dw.filter(p, &empty); !ok {
return nil
}
}
// todo: no need to validate if diff is trusted but is it always?
if err := destRoot.RemoveAll(base); err != nil {
return errors.Wrapf(err, "failed to remove: %s", destPath)
}
if dw.opt.NotifyCb != nil {
if err := dw.opt.NotifyCb(kind, p, nil, nil); err != nil {
return err
}
}
return nil
}
stat, ok := fi.Sys().(*types.Stat)
if !ok {
return errors.WithStack(&os.PathError{Path: p, Err: syscall.EBADMSG, Op: "change without stat info"})
}
statCopy := stat.Clone()
if dw.filter != nil {
if ok := dw.filter(p, statCopy); !ok {
return nil
}
}
rename := true
oldFi, err := destRoot.Lstat(base)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
if kind != ChangeKindAdd {
return errors.Wrap(err, "modify/rm")
}
rename = false
} else {
return errors.WithStack(err)
}
}
if oldFi != nil && fi.IsDir() && oldFi.IsDir() {
if err := rewriteRootMetadata(destRoot, base, statCopy); err != nil {
return errors.Wrapf(err, "error setting dir metadata for %s", destPath)
}
return nil
}
newPath := base
if rename {
newPath = ".tmp." + nextSuffix()
}
isRegularFile := false
switch {
case fi.IsDir():
if err := destRoot.Mkdir(newPath, fi.Mode().Perm()); err != nil {
if errors.Is(err, syscall.EEXIST) {
// we saw a race to create this directory, so try again
return dw.HandleChange(kind, p, fi, nil)
}
return errors.Wrapf(err, "failed to create dir %s", newPath)
}
dw.dirModTimes[filepath.ToSlash(destPath)] = statCopy.ModTime
case fi.Mode()&os.ModeDevice != 0 || fi.Mode()&os.ModeNamedPipe != 0:
if err := handleRootTarTypeBlockCharFifo(destRoot, newPath, statCopy); err != nil {
return errors.Wrapf(err, "failed to create device %s", newPath)
}
case fi.Mode()&os.ModeSymlink != 0:
if err := destRoot.Symlink(statCopy.Linkname, newPath); err != nil {
return errors.Wrapf(err, "failed to symlink %s", newPath)
}
case statCopy.Linkname != "":
linkNewName := destPath
if rename {
linkNewName = filepath.Join(filepath.Dir(destPath), newPath)
}
if err := dw.dest.Link(statCopy.Linkname, linkNewName); err != nil {
return errors.Wrapf(err, "failed to link %s to %s", newPath, statCopy.Linkname)
}
default:
isRegularFile = true
file, err := destRoot.OpenFile(newPath, os.O_CREATE|os.O_WRONLY, fi.Mode().Perm())
if err != nil {
return errors.Wrapf(err, "failed to create %s", newPath)
}
if dw.opt.SyncDataCb != nil {
if err := dw.processChange(dw.ctx, ChangeKindAdd, p, fi, file); err != nil {
file.Close()
return err
}
}
if err := file.Close(); err != nil {
return errors.Wrapf(err, "failed to close %s", newPath)
}
}
if err := rewriteRootMetadata(destRoot, newPath, statCopy); err != nil {
return errors.Wrapf(err, "error setting metadata for %s", newPath)
}
if rename {
if oldFi.IsDir() != fi.IsDir() {
if err := destRoot.RemoveAll(base); err != nil {
return errors.Wrapf(err, "failed to remove %s", destPath)
}
}
if err := destRoot.Rename(newPath, base); err != nil {
return errors.Wrapf(err, "failed to rename %s to %s", newPath, destPath)
}
}
if isRegularFile {
if dw.opt.AsyncDataCb != nil {
dw.requestAsyncFileData(p, destPath, fi, statCopy)
}
} else {
return dw.processChange(dw.ctx, kind, p, fi, nil)
}
return nil
}
func (dw *RootDiskWriter) requestAsyncFileData(p, dest string, fi os.FileInfo, st *types.Stat) {
// todo: limit worker threads
dw.eg.Go(func() error {
lease, err := dw.rootCache.get(dest)
if err != nil {
return err
}
defer lease.Release()
w := &rootLazyFileWriter{lease: lease}
if err := dw.processChange(dw.egCtx, ChangeKindAdd, p, fi, w); err != nil {
w.Close()
return err
}
return rootChtimes(lease.root, lease.base, st.ModTime) // TODO: parent dirs
})
}
func (dw *RootDiskWriter) processChange(ctx context.Context, kind ChangeKind, p string, fi os.FileInfo, w io.WriteCloser) error {
origw := w
var hw *hashedWriter
if dw.opt.NotifyCb != nil {
var err error
if hw, err = newHashWriter(dw.opt.ContentHasher, fi, w); err != nil {
return err
}
w = hw
}
if origw != nil {
fn := dw.opt.SyncDataCb
if fn == nil && dw.opt.AsyncDataCb != nil {
fn = dw.opt.AsyncDataCb
}
if err := fn(ctx, p, w); err != nil {
return err
}
} else {
if hw != nil {
hw.Close()
}
}
if hw != nil {
return dw.opt.NotifyCb(kind, p, hw, nil)
}
return nil
}
func cleanRootPath(p string) string {
if p == "" {
return "."
}
return filepath.Clean(p)
}
func rootChtimes(root Root, p string, un int64) error {
t := time.Unix(0, un)
if err := root.Chtimes(p, t, t); err != nil {
return errors.WithStack(err)
}
return nil
}
type rootLazyFileWriter struct {
lease *rootLease
f *os.File
fileMode *os.FileMode
closed bool
}
func (lfw *rootLazyFileWriter) Write(dt []byte) (int, error) {
if lfw.f == nil {
file, err := lfw.lease.root.OpenFile(lfw.lease.base, os.O_WRONLY, 0)
if os.IsPermission(err) {
// retry after chmod
fi, er := lfw.lease.root.Stat(lfw.lease.base)
if er == nil {
mode := fi.Mode()
lfw.fileMode = &mode
er = lfw.lease.root.Chmod(lfw.lease.base, mode|0222)
if er == nil {
file, err = lfw.lease.root.OpenFile(lfw.lease.base, os.O_WRONLY, 0)
}
}
}
if err != nil {
return 0, errors.Wrapf(err, "failed to open %s", lfw.lease.base)
}
lfw.f = file
}
return lfw.f.Write(dt)
}
func (lfw *rootLazyFileWriter) Close() error {
if lfw.closed {
return nil
}
lfw.closed = true
var err error
if lfw.f != nil {
err = lfw.f.Close()
}
if err == nil && lfw.fileMode != nil {
err = lfw.lease.root.Chmod(lfw.lease.base, *lfw.fileMode)
}
return err
}
+33
View File
@@ -0,0 +1,33 @@
//go:build !windows
package fsutil
import (
"os"
"time"
"github.com/pkg/errors"
"github.com/tonistiigi/fsutil/types"
)
func rewriteRootMetadata(root Root, p string, stat *types.Stat) error {
for key, value := range stat.Xattrs {
root.LSetxattr(p, key, value, 0)
}
if err := root.Lchown(p, int(stat.Uid), int(stat.Gid)); err != nil {
return errors.WithStack(err)
}
if os.FileMode(stat.Mode)&os.ModeSymlink != 0 {
return root.LChtimes(p, time.Unix(0, stat.ModTime))
}
if err := root.Chmod(p, os.FileMode(stat.Mode)); err != nil {
return errors.WithStack(err)
}
if err := rootChtimes(root, p, stat.ModTime); err != nil {
return err
}
return nil
}

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