vendor: update buildkit to f449174742bf

Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax
2026-06-10 16:41:09 +02:00
parent 2fcfce0e71
commit 1916210ddc
248 changed files with 29779 additions and 2300 deletions
+25 -25
View File
@@ -1,6 +1,6 @@
module github.com/docker/buildx module github.com/docker/buildx
go 1.25.5 go 1.25.9
require ( require (
github.com/Masterminds/semver/v3 v3.4.0 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/compose-spec/compose-go/v2 v2.10.2
github.com/containerd/console v1.0.5 github.com/containerd/console v1.0.5
github.com/containerd/containerd/v2 v2.2.4 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/errdefs v1.0.0
github.com/containerd/log v0.1.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/creack/pty v1.1.24
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/distribution/reference v0.6.0 github.com/distribution/reference v0.6.0
@@ -30,7 +30,7 @@ require (
github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/hcl/v2 v2.24.0
github.com/in-toto/in-toto-golang v0.11.0 github.com/in-toto/in-toto-golang v0.11.0
github.com/mitchellh/hashstructure/v2 v2.0.2 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/go-archive v0.2.0
github.com/moby/moby/api v1.54.2 github.com/moby/moby/api v1.54.2
github.com/moby/moby/client v0.4.1 github.com/moby/moby/client v0.4.1
@@ -41,7 +41,7 @@ require (
github.com/open-policy-agent/opa v1.10.1 github.com/open-policy-agent/opa v1.10.1
github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1 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/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 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/cobra v1.10.2
github.com/spf13/pflag v1.0.10 github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1 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/go-csvvalue v0.0.0-20240814133006-030d3b2625d0
github.com/tonistiigi/jaeger-ui-rest v0.0.0-20250408171107-3dd17559e117 github.com/tonistiigi/jaeger-ui-rest v0.0.0-20250408171107-3dd17559e117
github.com/zclconf/go-cty v1.17.0 github.com/zclconf/go-cty v1.17.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0
go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel v1.44.0
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0
go.opentelemetry.io/otel/metric v1.43.0 go.opentelemetry.io/otel/metric v1.44.0
go.opentelemetry.io/otel/sdk v1.43.0 go.opentelemetry.io/otel/sdk v1.44.0
go.opentelemetry.io/otel/trace v1.43.0 go.opentelemetry.io/otel/trace v1.44.0
go.yaml.in/yaml/v3 v3.0.4 go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.52.0 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/sync v0.20.0
golang.org/x/sys v0.45.0 golang.org/x/sys v0.45.0
golang.org/x/term v0.43.0 golang.org/x/term v0.43.0
golang.org/x/text v0.37.0 golang.org/x/text v0.37.0
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa
google.golang.org/grpc v1.80.0 google.golang.org/grpc v1.81.1
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.11
k8s.io/api v0.35.4 k8s.io/api v0.35.4
@@ -148,7 +148,7 @@ require (
github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-containerregistry v0.20.7 // indirect github.com/google/go-containerregistry v0.20.7 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // 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/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hiddeco/sshsig v0.2.0 // indirect github.com/hiddeco/sshsig v0.2.0 // indirect
github.com/in-toto/attestation v1.1.2 // 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/xhit/go-str2duration/v2 v2.1.0 // indirect
github.com/yashtewari/glob-intersection v0.2.0 // indirect github.com/yashtewari/glob-intersection v0.2.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // 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/google.golang.org/grpc/otelgrpc v0.69.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.68.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.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 go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect
golang.org/x/net v0.55.0 // indirect golang.org/x/net v0.55.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/time v0.15.0 // indirect golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.44.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/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.130.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/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 h1:8x2UdXqww7NYqGNabQ7i1nAgB5LegzjC9KQzO/900iA=
github.com/containerd/containerd/v2 v2.2.4/go.mod h1:YBcTO8D9149QY9zNmUjy04Mhuc4DlrZQ8FIOwKZEM7o= 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.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg=
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= 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 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
@@ -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/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 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 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.15 h1:kVYbFpYA4K43qxGVoc/VBwRXLAVWn4X9mdwGrR+HsLk=
github.com/containerd/nydus-snapshotter v0.15.13/go.mod h1:t95dwCb4I0RE4n1iOk0sJCWosNoACA8daOXmU5A2VHI= github.com/containerd/nydus-snapshotter v0.15.15/go.mod h1:L96yO+4iE6qqDiqXKhxMXBoPeaE7JgzXir9yanUVuOY=
github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4= github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4=
github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= 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 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y=
github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8= github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8=
github.com/containerd/stargz-snapshotter v0.18.2 h1:Ev/sxfQUjwzJQ9eqy3XzttcQ3osMIqkQgMYlcET+10M= 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/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 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/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.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= 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 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 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/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 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 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.20260610142556-f449174742bf h1:9qGH4EXFQgmqJUMjRxyX0zxA5L2x7CJiNHPAksuaF+U=
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/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 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= 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/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 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg=
github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 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.15.1 h1:ERxeh5caJvCzNAKdI8WQbJmB1LDTn4BuaAg8wihLBpA=
github.com/opencontainers/selinux v1.14.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ= 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 h1:KTRE0bK3sKbFKAk3yy63DpeskU7Cvs/x/Da5l+RtzyU=
github.com/package-url/packageurl-go v0.1.1/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c= 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/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.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 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 h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 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/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 h1:r0p7fK56l8WPequOaR3i9LBqfPtEdXIQbUTzT55iqT4=
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY= 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-20260609091201-0257b3308df4 h1:tJkv/edHw9FXVtbHxc6cpqDttiCLNzhqI1W40fcnxIY=
github.com/tonistiigi/fsutil v0.0.0-20260603212341-ed540e182f8a/go.mod h1:BKdcez7BiVtBvIcef90ZPc6ebqIWr4JWD7+EvLm6J98= 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 h1:2f304B10LaZdB8kkVEaoXvAMVan2tl9AiK4G0odjQtE=
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE= 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= 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/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 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= 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.3 h1:Cd46rkGXI3Td4yrVNwU8ripbxFaQbmesqhjBUUYAJSw=
github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= 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 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE=
github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= 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= 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.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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= 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.69.0 h1:2yEATaop1/a1I4psnSLgWVPLWwCzkqWakgJy7xTDVy0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= 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.68.0 h1:cuXaPAfIoJKsYjBjPSb2nKZEmgM43zVr25l37IxhKME= 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.68.0/go.mod h1:BuzhPofpCzlDi/Q/Xjg54M4/3oWqqyDe2Zeq7A2I0QE= 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.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0 h1:SUplec5dp06reu1zaXmOXdvqH398taqrDXqUl99jxSc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ= 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.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 h1:RuynHbfU8JUEw7DyONgkVYg2SVtsoF28y0LGIr69jgA=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0/go.mod h1:qZF+/lBs71APw8mlnEZcqZHMzqrYrsFiJOv83lX1OGo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= 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.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= 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.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= 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 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs= 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.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= 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 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.step.sm/crypto v0.77.2 h1:qFjjei+RHc5kP5R7NW9OUWT7SqWIuAOvOkXqg4fNWj8= 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/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 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 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.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= 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 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= 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= 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 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= 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-20260603202125-055de637280b h1:v1uXiEBHo8QA0LiGCo7UgHMzHT4Kdfpl2zmtH5vaP1Q=
golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/exp v0.0.0-20260603202125-055de637280b/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= 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 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= 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/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 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE=
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw= 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-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs= 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-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= 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 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= 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= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+1 -1
View File
@@ -216,7 +216,7 @@ func (c *context) verifyMetadata(resource, target Resource) error {
} }
if target.GID() != resource.GID() { 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 { 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 // 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 ( var (
m = syscallMode(mode.Perm()) m = syscallMode(mode.Perm())
dev uint64 dev uint64
) )
if mode&os.ModeDevice != 0 { if mode&os.ModeDevice != 0 {
dev = unix.Mkdev(uint32(maj), uint32(min)) dev = unix.Mkdev(uint32(major), uint32(minor))
if mode&os.ModeCharDevice != 0 { if mode&os.ModeCharDevice != 0 {
m |= unix.S_IFCHR m |= unix.S_IFCHR
+1 -1
View File
@@ -109,7 +109,7 @@ type LXAttrDriver interface {
} }
type DeviceInfoDriver 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" // 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 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) return devices.DeviceInfo(fi)
} }
+116
View File
@@ -26,6 +26,122 @@ import (
"golang.org/x/sys/unix" "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 { func copyFileInfo(fi os.FileInfo, src, name string) error {
st := fi.Sys().(*syscall.Stat_t) st := fi.Sys().(*syscall.Stat_t)
if err := os.Lchown(name, int(st.Uid), int(st.Gid)); err != nil { 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. 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 return false, nil
} }
maj, min, err := devices.DeviceInfo(f) major, minor, err := devices.DeviceInfo(f)
if err != nil { if err != nil {
return false, err return false, err
} }
return (maj == 0 && min == 0), nil return (major == 0 && minor == 0), nil
} }
if f.IsDir() { if f.IsDir() {
+4 -2
View File
@@ -20,11 +20,13 @@ package fs
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"os" "os"
"syscall" "syscall"
"github.com/containerd/continuity/sysx" "github.com/containerd/continuity/sysx"
"golang.org/x/sys/unix"
) )
// compareSysStat returns whether the stats are equivalent, // 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) { func compareCapabilities(p1, p2 string) (bool, error) {
c1, err := sysx.LGetxattr(p1, "security.capability") 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) return false, fmt.Errorf("failed to get xattr for %s: %w", p1, err)
} }
c2, err := sysx.LGetxattr(p2, "security.capability") 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 false, fmt.Errorf("failed to get xattr for %s: %w", p2, err)
} }
return bytes.Equal(c1, c2), nil 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 // 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 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. // 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 applyFn(func(root string) error {
fullPath := filepath.Join(root, name) 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. // 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 applyFn(func(root string) error {
return errors.New("Not implemented") return errors.New("Not implemented")
}) })
+15 -22
View File
@@ -1,32 +1,25 @@
version: "2"
linters: linters:
enable: enable:
- copyloopvar - copyloopvar
- gofmt - dupword
- goimports
- gosec - gosec
- ineffassign
- misspell - misspell
- nolintlint - nolintlint
- revive - revive
- staticcheck
- tenv # Detects using os.Setenv instead of t.Setenv since Go 1.17
- unconvert - unconvert
- unused
- govet
- dupword # Checks for duplicate words in the source code
disable: disable:
- errcheck - errcheck
exclusions:
run: generated: lax
timeout: 5m presets:
- comments
issues: - common-false-positives
exclude-dirs: - legacy
- api - std-error-handling
- cluster formatters:
- design enable:
- docs - gofmt
- docs/man - goimports
- releases exclusions:
- reports generated: lax
- test # e2e scripts
+105 -1
View File
@@ -152,6 +152,88 @@ func Only(platform specs.Platform) MatchComparer {
return Ordered(platformVector(Normalize(platform))...) 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. // OnlyStrict returns a match comparer for a single platform.
// //
// Unlike Only, OnlyStrict does not match sub platforms. // 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 return true
} }
if p1m || p2m { 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 return false
} }
} }
if len(p1.OSFeatures) > 0 || len(p2.OSFeatures) > 0 {
p1.OSFeatures = nil
p2.OSFeatures = nil
return c.Less(p1, p2)
}
return false return false
} }
@@ -242,9 +335,20 @@ func (c anyPlatformComparer) Less(p1, p2 specs.Platform) bool {
p2m = true p2m = true
} }
if p1m && p2m { 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 // If one matches, and the other does, sort match first
return p1m && !p2m 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 // 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 // by ourselves. We can just parse these information from /proc/cpuinfo
func getCPUInfo(pattern string) (info string, err error) { func getCPUInfo(pattern string) (info string, err error) {
cpuinfo, err := os.Open("/proc/cpuinfo") cpuinfo, err := os.Open("/proc/cpuinfo")
if err != nil { if err != nil {
return "", err return "", err
@@ -75,7 +74,6 @@ func getCPUInfo(pattern string) (info string, err error) {
// getCPUVariantFromArch get CPU variant from arch through a system call // getCPUVariantFromArch get CPU variant from arch through a system call
func getCPUVariantFromArch(arch string) (string, error) { func getCPUVariantFromArch(arch string) (string, error) {
var variant string var variant string
arch = strings.ToLower(arch) arch = strings.ToLower(arch)
+4 -4
View File
@@ -24,10 +24,10 @@ import (
) )
func getCPUVariant() (string, error) { func getCPUVariant() (string, error) {
var variant string 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 // Windows/Darwin only supports v7 for ARM32 and v8 for ARM64 and so we can use
// runtime.GOARCH to determine the variants // runtime.GOARCH to determine the variants
switch runtime.GOARCH { switch runtime.GOARCH {
@@ -38,7 +38,7 @@ func getCPUVariant() (string, error) {
default: default:
variant = "unknown" variant = "unknown"
} }
} else if runtime.GOOS == "freebsd" { case "freebsd":
// FreeBSD supports ARMv6 and ARMv7 as well as ARMv4 and ARMv5 (though deprecated) // FreeBSD supports ARMv6 and ARMv7 as well as ARMv4 and ARMv5 (though deprecated)
// detecting those variants is currently unimplemented // detecting those variants is currently unimplemented
switch runtime.GOARCH { switch runtime.GOARCH {
@@ -47,7 +47,7 @@ func getCPUVariant() (string, error) {
default: default:
variant = "unknown" variant = "unknown"
} }
} else { default:
return "", fmt.Errorf("getCPUVariant for OS %s: %v", runtime.GOOS, errNotImplemented) return "", fmt.Errorf("getCPUVariant for OS %s: %v", runtime.GOOS, errNotImplemented)
} }
+12
View File
@@ -17,6 +17,7 @@
package platforms package platforms
import ( import (
"slices"
"strconv" "strconv"
"strings" "strings"
@@ -162,3 +163,14 @@ func (c *windowsMatchComparer) Less(p1, p2 specs.Platform) bool {
} }
return m1 && !m2 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)
}
+168 -22
View File
@@ -111,9 +111,11 @@ package platforms
import ( import (
"fmt" "fmt"
"net/url"
"path" "path"
"regexp" "regexp"
"runtime" "runtime"
"slices"
"strconv" "strconv"
"strings" "strings"
@@ -121,12 +123,10 @@ import (
) )
var ( var (
specifierRe = regexp.MustCompile(`^[A-Za-z0-9_.-]+$`) 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. // Platform is a type alias for convenience, so there is no need to import image-spec package everywhere.
type Platform = specs.Platform type Platform = specs.Platform
@@ -143,6 +143,10 @@ type Matcher interface {
// functionality. // functionality.
// //
// Applications should opt to use `Match` over directly parsing specifiers. // 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 { func NewMatcher(platform specs.Platform) Matcher {
m := &matcher{ m := &matcher{
Platform: Normalize(platform), Platform: Normalize(platform),
@@ -152,6 +156,11 @@ func NewMatcher(platform specs.Platform) Matcher {
m.osvM = &windowsVersionMatcher{ m.osvM = &windowsVersionMatcher{
windowsOSVersion: getWindowsOSVersion(platform.OSVersion), 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 // In prior versions, on windows, the returned matcher implements a
// MatchComprarer interface. // MatchComprarer interface.
// This preserves that behavior for backwards compatibility. // 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 // 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. // `Less` that is implemented here ends up getting masked due to wrapping.
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
return &windowsMatchComparer{m} return &windowsMatchComparer{stripped}
} }
return stripped
} }
return m return m
} }
@@ -178,10 +188,39 @@ type matcher struct {
func (m *matcher) Match(platform specs.Platform) bool { func (m *matcher) Match(platform specs.Platform) bool {
normalized := Normalize(platform) normalized := Normalize(platform)
return m.OS == normalized.OS && if m.OS == normalized.OS &&
m.Architecture == normalized.Architecture && m.Architecture == normalized.Architecture &&
m.Variant == normalized.Variant && 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 { 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. // 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 // 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)` // system or architecture. The "os options" may be OSVersion which can be part of the OS
// When an OSVersion is specified, then specs.Platform.OSVersion is populated with that value, // like `windows(10.0.17763)`. When an OSVersion is specified, then specs.Platform.OSVersion is
// and an empty string otherwise. // 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 // If there is only a single string (no slashes), the
// value will be matched against the known set of operating systems, then fall // 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 // 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 var p specs.Platform
for i, part := range parts { for i, part := range parts {
if i == 0 { if i == 0 {
// First element is <os>[(<OSVersion>)] // First element is <os>[(<OSVersion>[+<OSFeature>]*)]
osVer := osAndVersionRe.FindStringSubmatch(part) osOptions := osRe.FindStringSubmatch(part)
if osVer == nil { 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, osAndVersionRe.String(), errInvalidArgument) 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.OS = normalizeOS(osOptions[1])
p.OSVersion = osVer[2] 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 { } else {
if !specifierRe.MatchString(part) { 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) 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) 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. // MustParse is like Parses but panics if the specifier cannot be parsed.
// Simplifies initialization of global variables. // Simplifies initialization of global variables.
func MustParse(specifier string) specs.Platform { func MustParse(specifier string) specs.Platform {
@@ -321,12 +397,77 @@ func FormatAll(platform specs.Platform) string {
if platform.OS == "" { if platform.OS == "" {
return "unknown" return "unknown"
} }
if platform.OSVersion == "" && len(platform.OSFeatures) == 0 {
if platform.OSVersion != "" { return path.Join(platform.OS, platform.Architecture, platform.Variant)
OSAndVersion := fmt.Sprintf(osAndVersionFormat, platform.OS, platform.OSVersion)
return path.Join(OSAndVersion, platform.Architecture, platform.Variant)
} }
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. // 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 { func Normalize(platform specs.Platform) specs.Platform {
platform.OS = normalizeOS(platform.OS) platform.OS = normalizeOS(platform.OS)
platform.Architecture, platform.Variant = normalizeArch(platform.Architecture, platform.Variant) 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 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//go:def.bzl", "go_library")
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_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"]) package(default_visibility = ["//visibility:public"])
+17 -1
View File
@@ -71,6 +71,7 @@ type ServeMux struct {
streamErrorHandler StreamErrorHandlerFunc streamErrorHandler StreamErrorHandlerFunc
routingErrorHandler RoutingErrorHandlerFunc routingErrorHandler RoutingErrorHandlerFunc
disablePathLengthFallback bool disablePathLengthFallback bool
disableHTTPMethodOverride bool
unescapingMode UnescapingMode unescapingMode UnescapingMode
writeContentLength bool writeContentLength bool
disableChunkedEncoding 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 // WithWriteContentLength returns a ServeMuxOption to enable writing content length on non-streaming responses
func WithWriteContentLength() ServeMuxOption { func WithWriteContentLength() ServeMuxOption {
return func(serveMux *ServeMux) { return func(serveMux *ServeMux) {
@@ -405,7 +419,7 @@ func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path = r.URL.RawPath 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 { if err := r.ParseForm(); err != nil {
_, outboundMarshaler := MarshalerForRequest(s, r) _, outboundMarshaler := MarshalerForRequest(s, r)
sterr := status.Error(codes.InvalidArgument, err.Error()) sterr := status.Error(codes.InvalidArgument, err.Error())
@@ -467,6 +481,7 @@ func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
HTTPStatus: http.StatusBadRequest, HTTPStatus: http.StatusBadRequest,
Err: mse, Err: mse,
}) })
return
} }
continue continue
} }
@@ -509,6 +524,7 @@ func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
HTTPStatus: http.StatusBadRequest, HTTPStatus: http.StatusBadRequest,
Err: mse, Err: mse,
}) })
return
} }
continue continue
} }
+24
View File
@@ -1,5 +1,11 @@
package client package client
import (
"strings"
"github.com/pkg/errors"
)
const ( const (
ExporterImage = "image" ExporterImage = "image"
ExporterLocal = "local" ExporterLocal = "local"
@@ -7,3 +13,21 @@ const (
ExporterOCI = "oci" ExporterOCI = "oci"
ExporterDocker = "docker" 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)
}
}
+16 -1
View File
@@ -189,7 +189,22 @@ func (c *Client) solve(ctx context.Context, def *llb.Definition, runGateway runG
if ex.OutputDir == "" { if ex.OutputDir == "" {
return nil, errors.Errorf("output directory is required for %s exporter", ex.Type) return nil, errors.Errorf("output directory is required for %s exporter", ex.Type)
} }
syncTargets = append(syncTargets, filesync.WithFSSyncDir(exID, ex.OutputDir)) 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 { if supportStore {
store := ex.OutputStore store := ex.OutputStore
+20 -4
View File
@@ -8,7 +8,6 @@ import (
"time" "time"
"github.com/moby/buildkit/util/bklog" "github.com/moby/buildkit/util/bklog"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/tonistiigi/fsutil" "github.com/tonistiigi/fsutil"
fstypes "github.com/tonistiigi/fsutil/types" 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 { if err := os.MkdirAll(dest, 0700); err != nil {
return errors.Wrapf(err, "failed to create synctarget dest dir %s", dest) 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, Merge: true,
Filter: func() func(string, *fstypes.Stat) bool { Filter: func() func(string, *fstypes.Stat) bool {
uid := os.Getuid() uid := os.Getuid()
@@ -126,7 +126,23 @@ func syncTargetDiffCopy(ds grpc.ServerStream, dest string) error {
return true 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 { func writeTargetFile(ds grpc.ServerStream, wc io.WriteCloser) error {
+22 -8
View File
@@ -253,9 +253,10 @@ type FSSyncTarget interface {
} }
type fsSyncTarget struct { type fsSyncTarget struct {
id int id int
outdir string outdir string
f FileOutputFunc deleteMode bool
f FileOutputFunc
} }
func (target *fsSyncTarget) target() *fsSyncTarget { func (target *fsSyncTarget) target() *fsSyncTarget {
@@ -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 { func NewFSSyncTarget(targets ...FSSyncTarget) *SyncTarget {
st := &SyncTarget{ st := &SyncTarget{
fs: make(map[int]FileOutputFunc), fs: make(map[int]FileOutputFunc),
outdirs: make(map[int]string), outdirs: make(map[int]fsSyncDirTarget),
} }
st.Add(targets...) st.Add(targets...)
return st return st
@@ -287,7 +301,7 @@ func NewFSSyncTarget(targets ...FSSyncTarget) *SyncTarget {
type SyncTarget struct { type SyncTarget struct {
fs map[int]FileOutputFunc fs map[int]FileOutputFunc
outdirs map[int]string outdirs map[int]fsSyncDirTarget
} }
var _ session.Attachable = &SyncTarget{} var _ session.Attachable = &SyncTarget{}
@@ -299,7 +313,7 @@ func (sp *SyncTarget) Add(targets ...FSSyncTarget) {
sp.fs[t.id] = t.f sp.fs[t.id] = t.f
} }
if t.outdir != "" { 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) { func (sp *SyncTarget) DiffCopy(stream FileSend_DiffCopyServer) (err error) {
id := sp.chooser(stream.Context()) id := sp.chooser(stream.Context())
if outdir, ok := sp.outdirs[id]; ok { if target, ok := sp.outdirs[id]; ok {
return syncTargetDiffCopy(stream, outdir) return syncTargetDiffCopy(stream, target.outdir, target.deleteMode)
} }
f, ok := sp.fs[id] f, ok := sp.fs[id]
if !ok { if !ok {
+2 -6
View File
@@ -30,7 +30,6 @@ const (
NetMode_UNSET NetMode = 0 // sandbox NetMode_UNSET NetMode = 0 // sandbox
NetMode_HOST NetMode = 1 NetMode_HOST NetMode = 1
NetMode_NONE NetMode = 2 NetMode_NONE NetMode = 2
NetMode_PROXY NetMode = 3
) )
// Enum value maps for NetMode. // Enum value maps for NetMode.
@@ -39,13 +38,11 @@ var (
0: "UNSET", 0: "UNSET",
1: "HOST", 1: "HOST",
2: "NONE", 2: "NONE",
3: "PROXY",
} }
NetMode_value = map[string]int32{ NetMode_value = map[string]int32{
"UNSET": 0, "UNSET": 0,
"HOST": 1, "HOST": 1,
"NONE": 2, "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" + "\x05upper\x18\x02 \x01(\v2\x12.pb.UpperDiffInputR\x05upper\"9\n" +
"\rPassthroughOp\x12\x0e\n" + "\rPassthroughOp\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x18\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" + "\aNetMode\x12\t\n" +
"\x05UNSET\x10\x00\x12\b\n" + "\x05UNSET\x10\x00\x12\b\n" +
"\x04HOST\x10\x01\x12\b\n" + "\x04HOST\x10\x01\x12\b\n" +
"\x04NONE\x10\x02\x12\t\n" + "\x04NONE\x10\x02*)\n" +
"\x05PROXY\x10\x03*)\n" +
"\fSecurityMode\x12\v\n" + "\fSecurityMode\x12\v\n" +
"\aSANDBOX\x10\x00\x12\f\n" + "\aSANDBOX\x10\x00\x12\f\n" +
"\bINSECURE\x10\x01*@\n" + "\bINSECURE\x10\x01*@\n" +
-1
View File
@@ -83,7 +83,6 @@ enum NetMode {
UNSET = 0; // sandbox UNSET = 0; // sandbox
HOST = 1; HOST = 1;
NONE = 2; NONE = 2;
PROXY = 3;
} }
enum SecurityMode { enum SecurityMode {
+1
View File
@@ -5,3 +5,4 @@ cmd/tomljson/tomljson
cmd/tomltestgen/tomltestgen cmd/tomltestgen/tomltestgen
dist dist
tests/ tests/
test-results
+33 -41
View File
@@ -1,84 +1,76 @@
[service] version = "2"
golangci-lint-version = "1.39.0"
[linters-settings.wsl]
allow-assign-and-anything = true
[linters-settings.exhaustive]
default-signifies-exhaustive = true
[linters] [linters]
disable-all = true default = "none"
enable = [ enable = [
"asciicheck", "asciicheck",
"bodyclose", "bodyclose",
"cyclop",
"deadcode",
"depguard",
"dogsled", "dogsled",
"dupl", "dupl",
"durationcheck", "durationcheck",
"errcheck", "errcheck",
"errorlint", "errorlint",
"exhaustive", "exhaustive",
# "exhaustivestruct",
"exportloopref",
"forbidigo", "forbidigo",
# "forcetypeassert",
"funlen",
"gci",
# "gochecknoglobals",
"gochecknoinits", "gochecknoinits",
"gocognit",
"goconst", "goconst",
"gocritic", "gocritic",
"gocyclo", "godoclint",
"godot",
"godox",
# "goerr113",
"gofmt",
"gofumpt",
"goheader", "goheader",
"goimports",
"golint",
"gomnd",
# "gomoddirectives",
"gomodguard", "gomodguard",
"goprintffuncname", "goprintffuncname",
"gosec", "gosec",
"gosimple",
"govet", "govet",
# "ifshort",
"importas", "importas",
"ineffassign", "ineffassign",
"lll", "lll",
"makezero", "makezero",
"mirror",
"misspell", "misspell",
"nakedret", "nakedret",
"nestif",
"nilerr", "nilerr",
# "nlreturn",
"noctx", "noctx",
"nolintlint", "nolintlint",
#"paralleltest", "perfsprint",
"prealloc", "prealloc",
"predeclared", "predeclared",
"revive", "revive",
"rowserrcheck", "rowserrcheck",
"sqlclosecheck", "sqlclosecheck",
"staticcheck", "staticcheck",
"structcheck",
"stylecheck",
# "testpackage",
"thelper", "thelper",
"tparallel", "tparallel",
"typecheck",
"unconvert", "unconvert",
"unparam", "unparam",
"unused", "unused",
"varcheck", "usetesting",
"wastedassign", "wastedassign",
"whitespace", "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 - linux_riscv64
- windows_amd64 - windows_amd64
- windows_arm64 - windows_arm64
- windows_arm
- darwin_amd64 - darwin_amd64
- darwin_arm64 - darwin_arm64
- id: tomljson - id: tomljson
@@ -42,7 +41,6 @@ builds:
- linux_riscv64 - linux_riscv64
- windows_amd64 - windows_amd64
- windows_arm64 - windows_arm64
- windows_arm
- darwin_amd64 - darwin_amd64
- darwin_arm64 - darwin_arm64
- id: jsontoml - id: jsontoml
@@ -62,7 +60,6 @@ builds:
- linux_arm - linux_arm
- windows_amd64 - windows_amd64
- windows_arm64 - windows_arm64
- windows_arm
- darwin_amd64 - darwin_amd64
- darwin_arm64 - darwin_arm64
universal_binaries: 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 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 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 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 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. 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 maintainer clicks on button, it is highly recommended that you run them locally
as you make changes. 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 ### Check coverage
We use `go tool cover` to compute test coverage. Most code editors have a way to 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 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 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. responsible for checking that your changes do not incur a performance penalty.
You can run their following to execute benchmarks: 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 1. Decide on the next version number. Use semver. Review commits since last
version to assess. version to assess.
2. Tag release. For example: 2. Tag release. For example:
``` ```
git checkout v2 git checkout v2
git pull git pull
git tag v2.2.0 git tag v2.2.0
git push --tags git push --tags
``` ```
3. CI automatically builds a draft Github release. Review it and edit as 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 necessary. Look for "Other changes". That would indicate a pull request not
labeled properly. Tweak labels and pull request titles until changelog looks labeled properly. Tweak labels and pull request titles until changelog looks
good for users. good for users.
+88 -44
View File
@@ -21,8 +21,6 @@ documentation.
import "github.com/pelletier/go-toml/v2" import "github.com/pelletier/go-toml/v2"
``` ```
See [Modules](#Modules).
## Features ## Features
### Stdlib behavior ### Stdlib behavior
@@ -107,7 +105,11 @@ type MyConfig struct {
### Unmarshaling ### Unmarshaling
[`Unmarshal`][unmarshal] reads a TOML document and fills a Go structure with its [`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 ```go
doc := ` doc := `
@@ -133,6 +135,62 @@ fmt.Println("tags:", cfg.Tags)
[unmarshal]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Unmarshal [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 ### Marshaling
[`Marshal`][marshal] is the opposite of Unmarshal: it represents a Go structure [`Marshal`][marshal] is the opposite of Unmarshal: it represents a Go structure
@@ -175,17 +233,17 @@ the AST level. See https://pkg.go.dev/github.com/pelletier/go-toml/v2/unstable.
Execution time speedup compared to other Go TOML libraries: Execution time speedup compared to other Go TOML libraries:
<table> <table>
<thead> <thead>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr> <tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead> </thead>
<tbody> <tbody>
<tr><td>Marshal/HugoFrontMatter-2</td><td>1.9x</td><td>2.2x</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>1.7x</td><td>2.1x</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.2x</td><td>3.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>2.9x</td><td>2.7x</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.6x</td><td>2.7x</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.6x</td><td>5.1x</td></tr> <tr><td>Unmarshal/ReferenceFile/struct-2</td><td>4.8x</td><td>5.0x</td></tr>
</tbody> </tbody>
</table> </table>
<details><summary>See more</summary> <details><summary>See more</summary>
<p>The table above has the results of the most common use-cases. The table below <p>The table above has the results of the most common use-cases. The table below
@@ -193,40 +251,26 @@ contains the results of all benchmarks, including unrealistic ones. It is
provided for completeness.</p> provided for completeness.</p>
<table> <table>
<thead> <thead>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr> <tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead> </thead>
<tbody> <tbody>
<tr><td>Marshal/SimpleDocument/map-2</td><td>1.8x</td><td>2.7x</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.7x</td><td>3.8x</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>3.8x</td><td>3.0x</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.6x</td><td>4.1x</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.0x</td><td>3.2x</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.3x</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.6x</td><td>2.7x</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.2x</td><td>2.3x</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.8x</td><td>1.5x</td></tr> <tr><td>UnmarshalDataset/canada-2</td><td>1.9x</td><td>1.5x</td></tr>
<tr><td>UnmarshalDataset/config-2</td><td>4.1x</td><td>2.9x</td></tr> <tr><td>UnmarshalDataset/config-2</td><td>5.4x</td><td>3.0x</td></tr>
<tr><td>geomean</td><td>2.7x</td><td>2.8x</td></tr> <tr><td>geomean</td><td>2.9x</td><td>2.8x</td></tr>
</tbody> </tbody>
</table> </table>
<p>This table can be generated with <code>./ci.sh benchmark -a -html</code>.</p> <p>This table can be generated with <code>./ci.sh benchmark -a -html</code>.</p>
</details> </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 ## Tools
Go-toml provides three handy command line tools: Go-toml provides three handy command line tools:
+6 -1
View File
@@ -147,7 +147,7 @@ bench() {
pushd "$dir" pushd "$dir"
if [ "${replace}" != "" ]; then 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}" go get "${replace}"
fi fi
@@ -195,6 +195,11 @@ for line in reversed(lines[2:]):
"%.1fx" % (float(line[3])/v2), # v1 "%.1fx" % (float(line[3])/v2), # v1
"%.1fx" % (float(line[7])/v2), # bs "%.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 # move geomean to the end
results.append(results[0]) results.append(results[0])
del results[0] del results[0]
+2 -3
View File
@@ -230,8 +230,8 @@ func parseLocalTime(b []byte) (LocalTime, []byte, error) {
return t, nil, err return t, nil, err
} }
if t.Second > 60 { if t.Second > 59 {
return t, nil, unstable.NewParserError(b[6:8], "seconds cannot be greater 60") return t, nil, unstable.NewParserError(b[6:8], "seconds cannot be greater than 59")
} }
b = b[8:] b = b[8:]
@@ -279,7 +279,6 @@ func parseLocalTime(b []byte) (LocalTime, []byte, error) {
return t, b, nil return t, b, nil
} }
//nolint:cyclop
func parseFloat(b []byte) (float64, error) { func parseFloat(b []byte) (float64, error) {
if len(b) == 4 && (b[0] == '+' || b[0] == '-') && b[1] == 'n' && b[2] == 'a' && b[3] == 'n' { if len(b) == 4 && (b[0] == '+' || b[0] == '-') && b[1] == 'n' && b[2] == 'a' && b[3] == 'n' {
return math.NaN(), nil return math.NaN(), nil
+35 -4
View File
@@ -2,10 +2,10 @@ package toml
import ( import (
"fmt" "fmt"
"reflect"
"strconv" "strconv"
"strings" "strings"
"github.com/pelletier/go-toml/v2/internal/danger"
"github.com/pelletier/go-toml/v2/unstable" "github.com/pelletier/go-toml/v2/unstable"
) )
@@ -54,6 +54,18 @@ func (s *StrictMissingError) String() string {
return buf.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 type Key []string
// Error returns the error message contained in the DecodeError. // Error returns the error message contained in the DecodeError.
@@ -78,7 +90,7 @@ func (e *DecodeError) Key() Key {
return e.key return e.key
} }
// decodeErrorFromHighlight creates a DecodeError referencing a highlighted // wrapDecodeError creates a DecodeError referencing a highlighted
// range of bytes from document. // range of bytes from document.
// //
// highlight needs to be a sub-slice of document, or this function panics. // highlight needs to be a sub-slice of document, or this function panics.
@@ -88,7 +100,7 @@ func (e *DecodeError) Key() Key {
// //
//nolint:funlen //nolint:funlen
func wrapDecodeError(document []byte, de *unstable.ParserError) *DecodeError { func wrapDecodeError(document []byte, de *unstable.ParserError) *DecodeError {
offset := danger.SubsliceOffset(document, de.Highlight) offset := subsliceOffset(document, de.Highlight)
errMessage := de.Error() errMessage := de.Error()
errLine, errColumn := positionAtEnd(document[:offset]) 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 package characters
var invalidAsciiTable = [256]bool{ var invalidASCIITable = [256]bool{
0x00: true, 0x00: true,
0x01: true, 0x01: true,
0x02: true, 0x02: true,
@@ -37,6 +37,6 @@ var invalidAsciiTable = [256]bool{
0x7F: true, 0x7F: true,
} }
func InvalidAscii(b byte) bool { func InvalidASCII(b byte) bool {
return invalidAsciiTable[b] return invalidASCIITable[b]
} }
+22 -46
View File
@@ -1,20 +1,12 @@
// Package characters provides functions for working with string encodings.
package characters package characters
import ( import (
"unicode/utf8" "unicode/utf8"
) )
type utf8Err struct { // Utf8TomlValidAlreadyEscaped verifies that a given string is only made of
Index int // valid UTF-8 characters allowed by the TOML spec:
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:
// //
// Any Unicode character may be used except those that must be escaped: // Any Unicode character may be used except those that must be escaped:
// quotation mark, backslash, and the control characters other than tab (U+0000 // 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 // It is a copy of the Go 1.17 utf8.Valid implementation, tweaked to exit early
// when a character is not allowed. // when a character is not allowed.
// //
// The returned utf8Err is Zero() if the string is valid, or contains the byte // The returned slice is empty if the string is valid, or contains the bytes
// index and size of the invalid character. // of the invalid character.
// //
// quotation mark => already checked // quotation mark => already checked
// backslash => already checked // backslash => already checked
@@ -32,9 +24,8 @@ func (u utf8Err) Zero() bool {
// 0x9 => tab, ok // 0x9 => tab, ok
// 0xA - 0x1F => invalid // 0xA - 0x1F => invalid
// 0x7F => 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. // Fast path. Check for and skip 8 bytes of ASCII characters per iteration.
offset := 0
for len(p) >= 8 { for len(p) >= 8 {
// Combining two 32 bit loads allows the same code to be used // Combining two 32 bit loads allows the same code to be used
// for 32 and 64 bit platforms. // for 32 and 64 bit platforms.
@@ -48,24 +39,19 @@ func Utf8TomlValidAlreadyEscaped(p []byte) (err utf8Err) {
} }
for i, b := range p[:8] { for i, b := range p[:8] {
if InvalidAscii(b) { if InvalidASCII(b) {
err.Index = offset + i return p[i : i+1]
err.Size = 1
return
} }
} }
p = p[8:] p = p[8:]
offset += 8
} }
n := len(p) n := len(p)
for i := 0; i < n; { for i := 0; i < n; {
pi := p[i] pi := p[i]
if pi < utf8.RuneSelf { if pi < utf8.RuneSelf {
if InvalidAscii(pi) { if InvalidASCII(pi) {
err.Index = offset + i return p[i : i+1]
err.Size = 1
return
} }
i++ i++
continue continue
@@ -73,44 +59,34 @@ func Utf8TomlValidAlreadyEscaped(p []byte) (err utf8Err) {
x := first[pi] x := first[pi]
if x == xx { if x == xx {
// Illegal starter byte. // Illegal starter byte.
err.Index = offset + i return p[i : i+1]
err.Size = 1
return
} }
size := int(x & 7) size := int(x & 7)
if i+size > n { if i+size > n {
// Short or invalid. // Short or invalid.
err.Index = offset + i return p[i:n]
err.Size = n - i
return
} }
accept := acceptRanges[x>>4] accept := acceptRanges[x>>4]
if c := p[i+1]; c < accept.lo || accept.hi < c { if c := p[i+1]; c < accept.lo || accept.hi < c {
err.Index = offset + i return p[i : i+2]
err.Size = 2 } else if size == 2 { //revive:disable:empty-block
return
} else if size == 2 {
} else if c := p[i+2]; c < locb || hicb < c { } else if c := p[i+2]; c < locb || hicb < c {
err.Index = offset + i return p[i : i+3]
err.Size = 3 } else if size == 3 { //revive:disable:empty-block
return
} else if size == 3 {
} else if c := p[i+3]; c < locb || hicb < c { } else if c := p[i+3]; c < locb || hicb < c {
err.Index = offset + i return p[i : i+4]
err.Size = 4
return
} }
i += size 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 { func Utf8ValidNext(p []byte) int {
c := p[0] c := p[0]
if c < utf8.RuneSelf { if c < utf8.RuneSelf {
if InvalidAscii(c) { if InvalidASCII(c) {
return 0 return 0
} }
return 1 return 1
@@ -129,10 +105,10 @@ func Utf8ValidNext(p []byte) int {
accept := acceptRanges[x>>4] accept := acceptRanges[x>>4]
if c := p[1]; c < accept.lo || accept.hi < c { if c := p[1]; c < accept.lo || accept.hi < c {
return 0 return 0
} else if size == 2 { } else if size == 2 { //nolint:revive
} else if c := p[2]; c < locb || hicb < c { } else if c := p[2]; c < locb || hicb < c {
return 0 return 0
} else if size == 3 { } else if size == 3 { //nolint:revive
} else if c := p[3]; c < locb || hicb < c { } else if c := p[3]; c < locb || hicb < c {
return 0 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 { func (t *KeyTracker) Key() []string {
k := make([]string, len(t.k)) k := make([]string, len(t.k))
copy(k, 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) idx = s.create(parentIdx, k, tableKind, false, true)
} else { } else {
entry := s.entries[idx] entry := s.entries[idx]
if it.IsLast() { switch {
case it.IsLast():
return false, fmt.Errorf("toml: key %s is already defined", string(k)) 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) 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)) 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) return s.checkInlineTable(value)
case unstable.Array: case unstable.Array:
return s.checkArray(value) return s.checkArray(value)
default:
return false, nil
} }
return false, nil
} }
func (s *SeenTracker) checkArray(node *unstable.Node) (first bool, err error) { func (s *SeenTracker) checkArray(node *unstable.Node) (first bool, err error) {
it := node.Children() it := node.Children()
for it.Next() { for it.Next() {
n := it.Node() n := it.Node()
switch n.Kind { switch n.Kind { //nolint:exhaustive
case unstable.InlineTable: case unstable.InlineTable:
first, err = s.checkInlineTable(n) first, err = s.checkInlineTable(n)
if err != nil { if err != nil {
+1
View File
@@ -1 +1,2 @@
// Package tracker provides functions for keeping track of AST nodes.
package tracker package tracker
+1 -1
View File
@@ -45,7 +45,7 @@ func (d *LocalDate) UnmarshalText(b []byte) error {
type LocalTime struct { type LocalTime struct {
Hour int // Hour of the day: [0; 24[ Hour int // Hour of the day: [0; 24[
Minute int // Minute of the hour: [0; 60[ 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[ Nanosecond int // Nanoseconds within the second: [0, 1000000000[
Precision int // Number of digits to display for Nanosecond. Precision int // Number of digits to display for Nanosecond.
} }
+117 -48
View File
@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"encoding" "encoding"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"math" "math"
@@ -42,7 +43,7 @@ type Encoder struct {
arraysMultiline bool arraysMultiline bool
indentSymbol string indentSymbol string
indentTables bool indentTables bool
marshalJsonNumbers bool marshalJSONNumbers bool
} }
// NewEncoder returns a new Encoder that writes to w. // NewEncoder returns a new Encoder that writes to w.
@@ -89,14 +90,14 @@ func (enc *Encoder) SetIndentTables(indent bool) *Encoder {
return enc 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. // float or integer instead of relying on TextMarshaler to emit a string.
// //
// *Unstable:* This method does not follow the compatibility guarantees of // *Unstable:* This method does not follow the compatibility guarantees of
// semver. It can be changed or removed without a new major version being // semver. It can be changed or removed without a new major version being
// issued. // issued.
func (enc *Encoder) SetMarshalJsonNumbers(indent bool) *Encoder { func (enc *Encoder) SetMarshalJSONNumbers(indent bool) *Encoder {
enc.marshalJsonNumbers = indent enc.marshalJSONNumbers = indent
return enc 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 "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 // The "commented" option prefixes the value and all its children with a comment
// symbol. // symbol.
// //
@@ -177,7 +180,7 @@ func (enc *Encoder) Encode(v interface{}) error {
ctx.inline = enc.tablesInline ctx.inline = enc.tablesInline
if v == nil { 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)) b, err := enc.encode(b, ctx, reflect.ValueOf(v))
@@ -196,6 +199,7 @@ func (enc *Encoder) Encode(v interface{}) error {
type valueOptions struct { type valueOptions struct {
multiline bool multiline bool
omitempty bool omitempty bool
omitzero bool
commented bool commented bool
comment string comment string
} }
@@ -266,16 +270,15 @@ func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, e
case LocalDateTime: case LocalDateTime:
return append(b, x.String()...), nil return append(b, x.String()...), nil
case json.Number: case json.Number:
if enc.marshalJsonNumbers { if enc.marshalJSONNumbers {
if x == "" { /// Useful zero value. if x == "" { /// Useful zero value.
return append(b, "0"...), nil return append(b, "0"...), nil
} else if v, err := x.Int64(); err == nil { } else if v, err := x.Int64(); err == nil {
return enc.encode(b, ctx, reflect.ValueOf(v)) return enc.encode(b, ctx, reflect.ValueOf(v))
} else if f, err := x.Float64(); err == nil { } else if f, err := x.Float64(); err == nil {
return enc.encode(b, ctx, reflect.ValueOf(f)) 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) return enc.encodeSlice(b, ctx, v)
case reflect.Interface: case reflect.Interface:
if v.IsNil() { 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()) 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: case reflect.Float32:
f := v.Float() f := v.Float()
if math.IsNaN(f) { switch {
case math.IsNaN(f):
b = append(b, "nan"...) b = append(b, "nan"...)
} else if f > math.MaxFloat32 { case f > math.MaxFloat32:
b = append(b, "inf"...) b = append(b, "inf"...)
} else if f < -math.MaxFloat32 { case f < -math.MaxFloat32:
b = append(b, "-inf"...) b = append(b, "-inf"...)
} else if math.Trunc(f) == f { case math.Trunc(f) == f:
b = strconv.AppendFloat(b, f, 'f', 1, 32) b = strconv.AppendFloat(b, f, 'f', 1, 32)
} else { default:
b = strconv.AppendFloat(b, f, 'f', -1, 32) b = strconv.AppendFloat(b, f, 'f', -1, 32)
} }
case reflect.Float64: case reflect.Float64:
f := v.Float() f := v.Float()
if math.IsNaN(f) { switch {
case math.IsNaN(f):
b = append(b, "nan"...) b = append(b, "nan"...)
} else if f > math.MaxFloat64 { case f > math.MaxFloat64:
b = append(b, "inf"...) b = append(b, "inf"...)
} else if f < -math.MaxFloat64 { case f < -math.MaxFloat64:
b = append(b, "-inf"...) b = append(b, "-inf"...)
} else if math.Trunc(f) == f { case math.Trunc(f) == f:
b = strconv.AppendFloat(b, f, 'f', 1, 64) b = strconv.AppendFloat(b, f, 'f', 1, 64)
} else { default:
b = strconv.AppendFloat(b, f, 'f', -1, 64) b = strconv.AppendFloat(b, f, 'f', -1, 64)
} }
case reflect.Bool: case reflect.Bool:
@@ -384,6 +389,31 @@ func shouldOmitEmpty(options valueOptions, v reflect.Value) bool {
return options.omitempty && isEmptyValue(v) 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) { func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v reflect.Value) ([]byte, error) {
var err error var err error
@@ -434,8 +464,9 @@ func isEmptyValue(v reflect.Value) bool {
return v.Float() == 0 return v.Float() == 0
case reflect.Interface, reflect.Ptr: case reflect.Interface, reflect.Ptr:
return v.IsNil() return v.IsNil()
default:
return false
} }
return false
} }
func isEmptyStruct(v reflect.Value) bool { 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 { func needsQuoting(v string) bool {
// TODO: vectorize // TODO: vectorize
for _, b := range []byte(v) { 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 return true
} }
} }
@@ -517,12 +548,26 @@ func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byt
del = 0x7f del = 0x7f
) )
for _, r := range []byte(v) { bv := []byte(v)
for i := 0; i < len(bv); i++ {
r := bv[i]
switch r { switch r {
case '\\': case '\\':
b = append(b, `\\`...) b = append(b, `\\`...)
case '"': case '"':
b = append(b, `\"`...) 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': case '\b':
b = append(b, `\b`...) b = append(b, `\b`...)
case '\f': case '\f':
@@ -559,9 +604,9 @@ func (enc *Encoder) encodeUnquotedKey(b []byte, v string) []byte {
return append(b, v...) 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 { if len(ctx.parentKey) == 0 {
return b, nil return b
} }
b = enc.encodeComment(ctx.indent, ctx.options.comment, 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"...) b = append(b, "]\n"...)
return b, nil return b
} }
//nolint:cyclop
func (enc *Encoder) encodeKey(b []byte, k string) []byte { func (enc *Encoder) encodeKey(b []byte, k string) []byte {
needsQuotation := false needsQuotation := false
cannotUseLiteral := 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) { func (enc *Encoder) keyToString(k reflect.Value) (string, error) {
keyType := k.Type() keyType := k.Type()
switch { if keyType.Implements(textMarshalerType) {
case keyType.Kind() == reflect.String:
return k.String(), nil
case keyType.Implements(textMarshalerType):
keyB, err := k.Interface().(encoding.TextMarshaler).MarshalText() keyB, err := k.Interface().(encoding.TextMarshaler).MarshalText()
if err != nil { if err != nil {
return "", fmt.Errorf("toml: error marshalling key %v from text: %w", k, err) return "", fmt.Errorf("toml: error marshalling key %v from text: %w", k, err)
} }
return string(keyB), nil 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 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 return strconv.FormatUint(k.Uint(), 10), nil
case keyType.Kind() == reflect.Float32: case reflect.Float32:
return strconv.FormatFloat(k.Float(), 'f', -1, 32), nil 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 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())
} }
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) { func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
@@ -657,8 +704,18 @@ func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte
for iter.Next() { for iter.Next() {
v := iter.Value() v := iter.Value()
if isNil(v) { // Handle nil values: convert nil pointers to zero value,
continue // 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()) k, err := enc.keyToString(iter.Key())
@@ -748,9 +805,8 @@ func walkStruct(ctx encoderCtx, t *table, v reflect.Value) {
walkStruct(ctx, t, f.Elem()) walkStruct(ctx, t, f.Elem())
} }
continue continue
} else {
k = fieldType.Name
} }
k = fieldType.Name
} }
if isNil(f) { if isNil(f) {
@@ -760,6 +816,7 @@ func walkStruct(ctx encoderCtx, t *table, v reflect.Value) {
options := valueOptions{ options := valueOptions{
multiline: opts.multiline, multiline: opts.multiline,
omitempty: opts.omitempty, omitempty: opts.omitempty,
omitzero: opts.omitzero,
commented: opts.commented, commented: opts.commented,
comment: fieldType.Tag.Get("comment"), comment: fieldType.Tag.Get("comment"),
} }
@@ -820,6 +877,7 @@ type tagOptions struct {
multiline bool multiline bool
inline bool inline bool
omitempty bool omitempty bool
omitzero bool
commented bool commented bool
} }
@@ -832,7 +890,7 @@ func parseTag(tag string) (string, tagOptions) {
} }
raw := tag[idx+1:] raw := tag[idx+1:]
tag = string(tag[:idx]) tag = tag[:idx]
for raw != "" { for raw != "" {
var o string var o string
i := strings.Index(raw, ",") i := strings.Index(raw, ",")
@@ -848,6 +906,8 @@ func parseTag(tag string) (string, tagOptions) {
opts.inline = true opts.inline = true
case "omitempty": case "omitempty":
opts.omitempty = true opts.omitempty = true
case "omitzero":
opts.omitzero = true
case "commented": case "commented":
opts.commented = true opts.commented = true
} }
@@ -866,10 +926,7 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro
} }
if !ctx.skipTableHeader { if !ctx.skipTableHeader {
b, err = enc.encodeTableHeader(ctx, b) b = enc.encodeTableHeader(ctx, b)
if err != nil {
return nil, err
}
if enc.indentTables && len(ctx.parentKey) > 0 { if enc.indentTables && len(ctx.parentKey) > 0 {
ctx.indent++ ctx.indent++
@@ -882,6 +939,9 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro
if shouldOmitEmpty(kv.Options, kv.Value) { if shouldOmitEmpty(kv.Options, kv.Value) {
continue continue
} }
if kv.Options.omitzero && shouldOmitZero(kv.Options, kv.Value) {
continue
}
hasNonEmptyKV = true hasNonEmptyKV = true
ctx.setKey(kv.Key) 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) { if shouldOmitEmpty(table.Options, table.Value) {
continue continue
} }
if table.Options.omitzero && shouldOmitZero(table.Options, table.Value) {
continue
}
if first { if first {
first = false first = false
if hasNonEmptyKV { if hasNonEmptyKV {
@@ -935,6 +998,9 @@ func (enc *Encoder) encodeTableInline(b []byte, ctx encoderCtx, t table) ([]byte
if shouldOmitEmpty(kv.Options, kv.Value) { if shouldOmitEmpty(kv.Options, kv.Value) {
continue continue
} }
if kv.Options.omitzero && shouldOmitZero(kv.Options, kv.Value) {
continue
}
if first { if first {
first = false first = false
@@ -963,11 +1029,14 @@ func willConvertToTable(ctx encoderCtx, v reflect.Value) bool {
if !v.IsValid() { if !v.IsValid() {
return false 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 return false
} }
t := v.Type()
switch t.Kind() { switch t.Kind() {
case reflect.Map, reflect.Struct: case reflect.Map, reflect.Struct:
return !ctx.inline return !ctx.inline
+16 -9
View File
@@ -1,7 +1,6 @@
package toml package toml
import ( import (
"github.com/pelletier/go-toml/v2/internal/danger"
"github.com/pelletier/go-toml/v2/internal/tracker" "github.com/pelletier/go-toml/v2/internal/tracker"
"github.com/pelletier/go-toml/v2/unstable" "github.com/pelletier/go-toml/v2/unstable"
) )
@@ -13,6 +12,9 @@ type strict struct {
key tracker.KeyTracker key tracker.KeyTracker
missing []unstable.ParserError missing []unstable.ParserError
// Reference to the document for computing key ranges.
doc []byte
} }
func (s *strict) EnterTable(node *unstable.Node) { 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{ s.missing = append(s.missing, unstable.ParserError{
Highlight: keyLocation(node), Highlight: s.keyLocation(node),
Message: "missing table", Message: "missing table",
Key: s.key.Key(), Key: s.key.Key(),
}) })
@@ -65,8 +67,8 @@ func (s *strict) MissingField(node *unstable.Node) {
} }
s.missing = append(s.missing, unstable.ParserError{ s.missing = append(s.missing, unstable.ParserError{
Highlight: keyLocation(node), Highlight: s.keyLocation(node),
Message: "missing field", Message: "unknown field",
Key: s.key.Key(), Key: s.key.Key(),
}) })
} }
@@ -88,7 +90,7 @@ func (s *strict) Error(doc []byte) error {
return err return err
} }
func keyLocation(node *unstable.Node) []byte { func (s *strict) keyLocation(node *unstable.Node) []byte {
k := node.Key() k := node.Key()
hasOne := k.Next() hasOne := k.Next()
@@ -96,12 +98,17 @@ func keyLocation(node *unstable.Node) []byte {
panic("should not be called with empty key") panic("should not be called with empty key")
} }
start := k.Node().Data // Get the range from the first key to the last key.
end := k.Node().Data firstRaw := k.Node().Raw
lastRaw := firstRaw
for k.Next() { 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" "time"
) )
var timeType = reflect.TypeOf((*time.Time)(nil)).Elem() // isZeroer is used to check if a type has a custom IsZero method.
var textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem() // This allows custom types to define their own zero-value semantics.
var textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() type isZeroer interface {
var mapStringInterfaceType = reflect.TypeOf(map[string]interface{}(nil)) IsZero() bool
var sliceInterfaceType = reflect.TypeOf([]interface{}(nil)) }
var stringType = reflect.TypeOf("")
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("")
)
+176 -52
View File
@@ -12,7 +12,6 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/pelletier/go-toml/v2/internal/danger"
"github.com/pelletier/go-toml/v2/internal/tracker" "github.com/pelletier/go-toml/v2/internal/tracker"
"github.com/pelletier/go-toml/v2/unstable" "github.com/pelletier/go-toml/v2/unstable"
) )
@@ -57,13 +56,18 @@ func (d *Decoder) DisallowUnknownFields() *Decoder {
// EnableUnmarshalerInterface allows to enable unmarshaler interface. // 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 // 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 // that don't have a straightforward TOML representation to provide their own
// decoding logic. // decoding logic.
// //
// Currently, types can only decode from a single value. Tables and array tables // The UnmarshalTOML method receives raw TOML bytes:
// are not supported. // - 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 // *Unstable:* This method does not follow the compatibility guarantees of
// semver. It can be changed or removed without a new major version being // 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{ dec := decoder{
strict: strict{ strict: strict{
Enabled: d.strict, Enabled: d.strict,
doc: b,
}, },
unmarshalerInterface: d.unmarshalerInterface, unmarshalerInterface: d.unmarshalerInterface,
} }
@@ -226,7 +231,7 @@ func (d *decoder) FromParser(v interface{}) error {
} }
if r.IsNil() { 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() r = r.Elem()
@@ -273,7 +278,7 @@ func (d *decoder) handleRootExpression(expr *unstable.Node, v reflect.Value) err
var err error var err error
var first bool // used for to clear array tables on first use 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) first, err = d.seen.CheckExpression(expr)
if err != nil { if err != nil {
return err return err
@@ -378,7 +383,7 @@ func (d *decoder) handleArrayTableCollectionLast(key unstable.Iterator, v reflec
case reflect.Array: case reflect.Array:
idx := d.arrayIndex(true, v) idx := d.arrayIndex(true, v)
if idx >= v.Len() { 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) elem := v.Index(idx)
_, err := d.handleArrayTable(key, elem) _, err := d.handleArrayTable(key, elem)
@@ -416,27 +421,51 @@ func (d *decoder) handleArrayTableCollection(key unstable.Iterator, v reflect.Va
return v, nil return v, nil
case reflect.Slice: 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) x, err := d.handleArrayTable(key, elem)
if err != nil || d.skipUntilTable { if err != nil || d.skipUntilTable {
return reflect.Value{}, err return reflect.Value{}, err
} }
if x.IsValid() { if x.IsValid() {
elem.Set(x) if created {
elem = x
} else {
elem.Set(x)
}
} }
if created {
return reflect.Append(v, elem), nil
}
return v, err return v, err
case reflect.Array: case reflect.Array:
idx := d.arrayIndex(false, v) idx := d.arrayIndex(false, v)
if idx >= v.Len() { 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) elem := v.Index(idx)
_, err := d.handleArrayTable(key, elem) _, err := d.handleArrayTable(key, elem)
return v, err return v, err
default:
return d.handleArrayTable(key, v)
} }
return d.handleArrayTable(key, v)
} }
func (d *decoder) handleKeyPart(key unstable.Iterator, v reflect.Value, nextFn handlerFn, makeFn valueMakerFn) (reflect.Value, error) { 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) mv := v.MapIndex(mk)
set := false set := false
if !mv.IsValid() { switch {
case !mv.IsValid():
// If there is no value in the map, create a new one according to // 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 // the map type. If the element type is interface, create either a
// map[string]interface{} or a []interface{} depending on whether // 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() mv = reflect.New(t).Elem()
} }
set = true set = true
} else if mv.Kind() == reflect.Interface { case mv.Kind() == reflect.Interface:
mv = mv.Elem() mv = mv.Elem()
if !mv.IsValid() { if !mv.IsValid() {
mv = makeFn() mv = makeFn()
} }
set = true set = true
} else if !mv.CanAddr() { case !mv.CanAddr():
vt := v.Type() vt := v.Type()
t := vt.Elem() t := vt.Elem()
oldmv := mv oldmv := mv
@@ -574,18 +604,28 @@ func (d *decoder) handleArrayTablePart(key unstable.Iterator, v reflect.Value) (
// cannot handle it. // cannot handle it.
func (d *decoder) handleTable(key unstable.Iterator, v reflect.Value) (reflect.Value, error) { func (d *decoder) handleTable(key unstable.Iterator, v reflect.Value) (reflect.Value, error) {
if v.Kind() == reflect.Slice { if v.Kind() == reflect.Slice {
if v.Len() == 0 { // For non-empty slices, work with the last element
return reflect.Value{}, unstable.NewParserError(key.Node().Data, "cannot store a table in a slice") if v.Len() > 0 {
elem := v.Index(v.Len() - 1)
x, err := d.handleTable(key, elem)
if err != nil {
return reflect.Value{}, err
}
if x.IsValid() {
elem.Set(x)
}
return reflect.Value{}, nil
} }
elem := v.Index(v.Len() - 1) // Empty slice - check if it implements Unmarshaler (e.g., RawMessage)
x, err := d.handleTable(key, elem) // and we're at the end of the key path
if err != nil { if d.unmarshalerInterface && !key.Next() {
return reflect.Value{}, err if v.CanAddr() && v.Addr().CanInterface() {
if outi, ok := v.Addr().Interface().(unstable.Unmarshaler); ok {
return d.handleKeyValuesUnmarshaler(outi)
}
}
} }
if x.IsValid() { return reflect.Value{}, unstable.NewParserError(key.Node().Data, "cannot store a table in a slice")
elem.Set(x)
}
return reflect.Value{}, nil
} }
if key.Next() { if key.Next() {
// Still scoping the key // Still scoping the key
@@ -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 // Handle root expressions until the end of the document or the next
// non-key-value. // non-key-value.
func (d *decoder) handleKeyValues(v reflect.Value) (reflect.Value, error) { 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 var rv reflect.Value
for d.nextExpr() { for d.nextExpr() {
expr := d.expr() expr := d.expr()
@@ -628,6 +686,41 @@ func (d *decoder) handleKeyValues(v reflect.Value) (reflect.Value, error) {
return rv, nil 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 ( type (
handlerFn func(key unstable.Iterator, v reflect.Value) (reflect.Value, error) handlerFn func(key unstable.Iterator, v reflect.Value) (reflect.Value, error)
valueMakerFn func() reflect.Value valueMakerFn func() reflect.Value
@@ -672,14 +765,21 @@ func (d *decoder) handleValue(value *unstable.Node, v reflect.Value) error {
if d.unmarshalerInterface { if d.unmarshalerInterface {
if v.CanAddr() && v.Addr().CanInterface() { if v.CanAddr() && v.Addr().CanInterface() {
if outi, ok := v.Addr().Interface().(unstable.Unmarshaler); ok { 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))
} }
} }
} }
ok, err := d.tryTextUnmarshaler(value, v) // Only try TextUnmarshaler for scalar types. For Array and InlineTable,
if ok || err != nil { // fall through to struct/map unmarshaling to allow flexible unmarshaling
return err // 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 { switch value.Kind {
@@ -821,6 +921,9 @@ func (d *decoder) unmarshalDateTime(value *unstable.Node, v reflect.Value) error
return err 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)) v.Set(reflect.ValueOf(dt))
return nil return nil
} }
@@ -831,14 +934,14 @@ func (d *decoder) unmarshalLocalDate(value *unstable.Node, v reflect.Value) erro
return err 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 { if v.Type() == timeType {
cast := ld.AsTime(time.Local) v.Set(reflect.ValueOf(ld.AsTime(time.Local)))
v.Set(reflect.ValueOf(cast))
return nil return nil
} }
v.Set(reflect.ValueOf(ld)) v.Set(reflect.ValueOf(ld))
return nil 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") 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)) v.Set(reflect.ValueOf(lt))
return nil 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") 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 { if v.Type() == timeType {
cast := ldt.AsTime(time.Local) v.Set(reflect.ValueOf(ldt.AsTime(time.Local)))
v.Set(reflect.ValueOf(cast))
return nil return nil
} }
v.Set(reflect.ValueOf(ldt)) v.Set(reflect.ValueOf(ldt))
return nil return nil
} }
@@ -929,8 +1034,9 @@ const (
// compile time, so it is computed during initialization. // compile time, so it is computed during initialization.
var maxUint int64 = math.MaxInt64 var maxUint int64 = math.MaxInt64
func init() { func init() { //nolint:gochecknoinits
m := uint64(^uint(0)) m := uint64(^uint(0))
// #nosec G115
if m < uint64(maxUint) { if m < uint64(maxUint) {
maxUint = int64(m) maxUint = int64(m)
} }
@@ -1010,7 +1116,7 @@ func (d *decoder) unmarshalInteger(value *unstable.Node, v reflect.Value) error
case reflect.Interface: case reflect.Interface:
r = reflect.ValueOf(i) r = reflect.ValueOf(i)
default: 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()) { if !r.Type().AssignableTo(v.Type()) {
@@ -1029,7 +1135,7 @@ func (d *decoder) unmarshalString(value *unstable.Node, v reflect.Value) error {
case reflect.Interface: case reflect.Interface:
v.Set(reflect.ValueOf(string(value.Data))) v.Set(reflect.ValueOf(string(value.Data)))
default: 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 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 reflect.Value{}, fmt.Errorf("toml: error unmarshalling key type %s from text: %w", stringType, err)
} }
return mk.Elem(), nil 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) key, err := strconv.ParseInt(string(data), 10, 64)
if err != nil { if err != nil {
return reflect.Value{}, fmt.Errorf("toml: error parsing key of type %s from integer: %w", stringType, err) return reflect.Value{}, fmt.Errorf("toml: error parsing key of type %s from integer: %w", stringType, err)
} }
return reflect.ValueOf(key).Convert(keyType), nil 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) key, err := strconv.ParseUint(string(data), 10, 64)
if err != nil { if err != nil {
return reflect.Value{}, fmt.Errorf("toml: error parsing key of type %s from unsigned integer: %w", stringType, err) 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 return reflect.ValueOf(key).Convert(keyType), nil
case keyType.Kind() == reflect.Float32: case reflect.Float32:
key, err := strconv.ParseFloat(string(data), 32) key, err := strconv.ParseFloat(string(data), 32)
if err != nil { if err != nil {
return reflect.Value{}, fmt.Errorf("toml: error parsing key of type %s from float: %w", stringType, err) return reflect.Value{}, fmt.Errorf("toml: error parsing key of type %s from float: %w", stringType, err)
} }
return reflect.ValueOf(float32(key)), nil return reflect.ValueOf(float32(key)), nil
case keyType.Kind() == reflect.Float64: case reflect.Float64:
key, err := strconv.ParseFloat(string(data), 64) key, err := strconv.ParseFloat(string(data), 64)
if err != nil { if err != nil {
return reflect.Value{}, fmt.Errorf("toml: error parsing key of type %s from float: %w", stringType, err) return reflect.Value{}, fmt.Errorf("toml: error parsing key of type %s from float: %w", stringType, err)
} }
return reflect.ValueOf(float64(key)), nil 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)
} }
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) { 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: case reflect.Struct:
path, found := structFieldPath(v, string(key.Node().Data)) path, found := structFieldPath(v, string(key.Node().Data))
if !found { 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 d.skipUntilTable = true
break break
} }
@@ -1259,13 +1381,13 @@ func fieldByIndex(v reflect.Value, path []int) reflect.Value {
type fieldPathsMap = map[string][]int 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) { func structFieldPath(v reflect.Value, name string) ([]int, bool) {
t := v.Type() t := v.Type()
cache, _ := globalFieldPathsCache.Load().(map[danger.TypeID]fieldPathsMap) cache, _ := globalFieldPathsCache.Load().(map[reflect.Type]fieldPathsMap)
fieldPaths, ok := cache[danger.MakeTypeID(t)] fieldPaths, ok := cache[t]
if !ok { if !ok {
fieldPaths = map[string][]int{} fieldPaths = map[string][]int{}
@@ -1276,8 +1398,8 @@ func structFieldPath(v reflect.Value, name string) ([]int, bool) {
fieldPaths[strings.ToLower(name)] = path fieldPaths[strings.ToLower(name)] = path
}) })
newCache := make(map[danger.TypeID]fieldPathsMap, len(cache)+1) newCache := make(map[reflect.Type]fieldPathsMap, len(cache)+1)
newCache[danger.MakeTypeID(t)] = fieldPaths newCache[t] = fieldPaths
for k, v := range cache { for k, v := range cache {
newCache[k] = v newCache[k] = v
} }
@@ -1301,7 +1423,9 @@ func forEachField(t reflect.Type, path []int, do func(name string, path []int))
continue 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)] fieldPath = fieldPath[:len(fieldPath):len(fieldPath)]
name := f.Tag.Get("toml") name := f.Tag.Get("toml")
+42 -29
View File
@@ -1,10 +1,8 @@
package unstable package unstable
import ( import (
"errors"
"fmt" "fmt"
"unsafe"
"github.com/pelletier/go-toml/v2/internal/danger"
) )
// Iterator over a sequence of nodes. // Iterator over a sequence of nodes.
@@ -19,30 +17,43 @@ import (
// // do something with n // // do something with n
// } // }
type Iterator struct { type Iterator struct {
nodes *[]Node
idx int32
started bool started bool
node *Node
} }
// Next moves the iterator forward and returns true if points to a // Next moves the iterator forward and returns true if points to a
// node, false otherwise. // node, false otherwise.
func (c *Iterator) Next() bool { func (c *Iterator) Next() bool {
if c.nodes == nil {
return false
}
nodes := *c.nodes
if !c.started { if !c.started {
c.started = true c.started = true
} else if c.node.Valid() { } else {
c.node = c.node.Next() 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 // IsLast returns true if the current node of the iterator is the last
// one. Subsequent calls to Next() will return false. // one. Subsequent calls to Next() will return false.
func (c *Iterator) IsLast() bool { 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. // Node returns a pointer to the node pointed at by the iterator.
func (c *Iterator) Node() *Node { 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. // Node in a TOML expression AST.
@@ -65,11 +76,12 @@ type Node struct {
Raw Range // Raw bytes from the input. Raw Range // Raw bytes from the input.
Data []byte // Node value (either allocated or referencing the input). Data []byte // Node value (either allocated or referencing the input).
// References to other nodes, as offsets in the backing array // Absolute indices into the backing nodes slice. -1 means none.
// from this node. References can go backward, so those can be next int32
// negative. child int32
next int // 0 if last element
child int // 0 if no child // Reference to the backing nodes slice for navigation.
nodes *[]Node
} }
// Range of bytes in the document. // 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. // Next returns a pointer to the next node, or nil if there is no next node.
func (n *Node) Next() *Node { func (n *Node) Next() *Node {
if n.next == 0 { if n.next < 0 {
return nil return nil
} }
ptr := unsafe.Pointer(n) next := &(*n.nodes)[n.next]
size := unsafe.Sizeof(Node{}) next.nodes = n.nodes
return (*Node)(danger.Stride(ptr, size, n.next)) return next
} }
// Child returns a pointer to the first child node of this node. Other children // 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. // has no child.
func (n *Node) Child() *Node { func (n *Node) Child() *Node {
if n.child == 0 { if n.child < 0 {
return nil return nil
} }
ptr := unsafe.Pointer(n) child := &(*n.nodes)[n.child]
size := unsafe.Sizeof(Node{}) child.nodes = n.nodes
return (*Node)(danger.Stride(ptr, size, n.child)) return child
} }
// Valid returns true if the node's kind is set (not to Invalid). // 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 { func (n *Node) Key() Iterator {
switch n.Kind { switch n.Kind {
case KeyValue: case KeyValue:
value := n.Child() child := n.child
if !value.Valid() { if child < 0 {
panic(fmt.Errorf("KeyValue should have at least two children")) 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: case Table, ArrayTable:
return Iterator{node: n.Child()} return Iterator{nodes: n.nodes, idx: n.child}
default: default:
panic(fmt.Errorf("Key() is not supported on a %s", n.Kind)) 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. // Children returns an iterator over a node's children.
func (n *Node) Children() Iterator { 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 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 { func (r *root) at(idx reference) *Node {
return &r.nodes[idx] return &r.nodes[idx]
} }
@@ -33,12 +24,10 @@ type builder struct {
lastIdx int lastIdx int
} }
func (b *builder) Tree() *root {
return &b.tree
}
func (b *builder) NodeAt(ref reference) *Node { 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() { func (b *builder) Reset() {
@@ -48,24 +37,28 @@ func (b *builder) Reset() {
func (b *builder) Push(n Node) reference { func (b *builder) Push(n Node) reference {
b.lastIdx = len(b.tree.nodes) b.lastIdx = len(b.tree.nodes)
n.next = -1
n.child = -1
b.tree.nodes = append(b.tree.nodes, n) b.tree.nodes = append(b.tree.nodes, n)
return reference(b.lastIdx) return reference(b.lastIdx)
} }
func (b *builder) PushAndChain(n Node) reference { func (b *builder) PushAndChain(n Node) reference {
newIdx := len(b.tree.nodes) newIdx := len(b.tree.nodes)
n.next = -1
n.child = -1
b.tree.nodes = append(b.tree.nodes, n) b.tree.nodes = append(b.tree.nodes, n)
if b.lastIdx >= 0 { 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 b.lastIdx = newIdx
return reference(b.lastIdx) return reference(b.lastIdx)
} }
func (b *builder) AttachChild(parent reference, child reference) { 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) { 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 type Kind int
const ( const (
// Meta // Invalid represents an invalid meta node.
Invalid Kind = iota Invalid Kind = iota
// Comment represents a comment meta node.
Comment Comment
// Key represents a key meta node.
Key Key
// Top level structures // Table represents a top-level table.
Table Table
// ArrayTable represents a top-level array table.
ArrayTable ArrayTable
// KeyValue represents a top-level key value.
KeyValue KeyValue
// Containers values // Array represents an array container value.
Array Array
// InlineTable represents an inline table container value.
InlineTable InlineTable
// Values // String represents a string value.
String String
// Bool represents a boolean value.
Bool Bool
// Float represents a floating point value.
Float Float
// Integer represents an integer value.
Integer Integer
// LocalDate represents a a local date value.
LocalDate LocalDate
// LocalTime represents a local time value.
LocalTime LocalTime
// LocalDateTime represents a local date/time value.
LocalDateTime LocalDateTime
// DateTime represents a data/time value.
DateTime DateTime
) )
+71 -41
View File
@@ -3,10 +3,10 @@ package unstable
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"reflect"
"unicode" "unicode"
"github.com/pelletier/go-toml/v2/internal/characters" "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. // ParserError describes an error relative to the content of the document.
@@ -70,11 +70,34 @@ func (p *Parser) Data() []byte {
// panics. // panics.
func (p *Parser) Range(b []byte) Range { func (p *Parser) Range(b []byte) Range {
return Range{ return Range{
Offset: uint32(danger.SubsliceOffset(p.data, b)), Offset: uint32(p.subsliceOffset(b)), //nolint:gosec // TOML documents are small
Length: uint32(len(b)), 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. // Raw returns the slice corresponding to the bytes in the given range.
func (p *Parser) Raw(raw Range) []byte { func (p *Parser) Raw(raw Range) []byte {
return p.data[raw.Offset : raw.Offset+raw.Length] return p.data[raw.Offset : raw.Offset+raw.Length]
@@ -158,9 +181,17 @@ type Shape struct {
End Position End Position
} }
func (p *Parser) position(b []byte) Position { // Shape returns the shape of the given range in the input. Will
offset := danger.SubsliceOffset(p.data, b) // 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] lead := p.data[:offset]
return Position{ 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) { func (p *Parser) parseNewline(b []byte) ([]byte, error) {
if b[0] == '\n' { if b[0] == '\n' {
return b[1:], nil return b[1:], nil
@@ -199,7 +220,7 @@ func (p *Parser) parseComment(b []byte) (reference, []byte, error) {
if p.KeepComments && err == nil { if p.KeepComments && err == nil {
ref = p.builder.Push(Node{ ref = p.builder.Push(Node{
Kind: Comment, Kind: Comment,
Raw: p.Range(data), Raw: p.rangeOfToken(data, rest),
Data: data, Data: data,
}) })
} }
@@ -316,6 +337,9 @@ func (p *Parser) parseStdTable(b []byte) (reference, []byte, error) {
func (p *Parser) parseKeyval(b []byte) (reference, []byte, error) { func (p *Parser) parseKeyval(b []byte) (reference, []byte, error) {
// keyval = key keyval-sep val // keyval = key keyval-sep val
// Track the start position for Raw range
startB := b
ref := p.builder.Push(Node{ ref := p.builder.Push(Node{
Kind: KeyValue, Kind: KeyValue,
}) })
@@ -330,7 +354,7 @@ func (p *Parser) parseKeyval(b []byte) (reference, []byte, error) {
b = p.parseWhitespace(b) b = p.parseWhitespace(b)
if len(b) == 0 { 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) b, err = expect('=', b)
@@ -348,6 +372,11 @@ func (p *Parser) parseKeyval(b []byte) (reference, []byte, error) {
p.builder.Chain(valRef, key) p.builder.Chain(valRef, key)
p.builder.AttachChild(ref, valRef) 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 return ref, b, err
} }
@@ -376,7 +405,7 @@ func (p *Parser) parseVal(b []byte) (reference, []byte, error) {
if err == nil { if err == nil {
ref = p.builder.Push(Node{ ref = p.builder.Push(Node{
Kind: String, Kind: String,
Raw: p.Range(raw), Raw: p.rangeOfToken(raw, b),
Data: v, Data: v,
}) })
} }
@@ -394,7 +423,7 @@ func (p *Parser) parseVal(b []byte) (reference, []byte, error) {
if err == nil { if err == nil {
ref = p.builder.Push(Node{ ref = p.builder.Push(Node{
Kind: String, Kind: String,
Raw: p.Range(raw), Raw: p.rangeOfToken(raw, b),
Data: v, 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 ] // inline-table-keyvals = keyval [ inline-table-sep inline-table-keyvals ]
parent := p.builder.Push(Node{ parent := p.builder.Push(Node{
Kind: InlineTable, Kind: InlineTable,
Raw: p.Range(b[:1]), Raw: p.rangeOfToken(b[:1], b[1:]),
}) })
first := true first := true
@@ -542,7 +571,7 @@ func (p *Parser) parseValArray(b []byte) (reference, []byte, error) {
var err error var err error
for len(b) > 0 { for len(b) > 0 {
cref := invalidReference var cref reference
cref, b, err = p.parseOptionalWhitespaceCommentNewline(b) cref, b, err = p.parseOptionalWhitespaceCommentNewline(b)
if err != nil { if err != nil {
return parent, nil, err return parent, nil, err
@@ -611,12 +640,13 @@ func (p *Parser) parseOptionalWhitespaceCommentNewline(b []byte) (reference, []b
latestCommentRef := invalidReference latestCommentRef := invalidReference
addComment := func(ref reference) { addComment := func(ref reference) {
if rootCommentRef == invalidReference { switch {
case rootCommentRef == invalidReference:
rootCommentRef = ref rootCommentRef = ref
} else if latestCommentRef == invalidReference { case latestCommentRef == invalidReference:
p.builder.AttachChild(rootCommentRef, ref) p.builder.AttachChild(rootCommentRef, ref)
latestCommentRef = ref latestCommentRef = ref
} else { default:
p.builder.Chain(latestCommentRef, ref) p.builder.Chain(latestCommentRef, ref)
latestCommentRef = ref latestCommentRef = ref
} }
@@ -704,11 +734,11 @@ func (p *Parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, er
if !escaped { if !escaped {
str := token[startIdx:endIdx] str := token[startIdx:endIdx]
verr := characters.Utf8TomlValidAlreadyEscaped(str) highlight := characters.Utf8TomlValidAlreadyEscaped(str)
if verr.Zero() { if len(highlight) == 0 {
return token, str, rest, nil 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 var builder bytes.Buffer
@@ -744,7 +774,7 @@ func (p *Parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, er
i += j i += j
for ; i < len(token)-3; i++ { for ; i < len(token)-3; i++ {
c := token[i] c := token[i]
if !(c == '\n' || c == '\r' || c == ' ' || c == '\t') { if c != '\n' && c != '\r' && c != ' ' && c != '\t' {
i-- i--
break break
} }
@@ -820,7 +850,7 @@ func (p *Parser) parseKey(b []byte) (reference, []byte, error) {
ref := p.builder.Push(Node{ ref := p.builder.Push(Node{
Kind: Key, Kind: Key,
Raw: p.Range(raw), Raw: p.rangeOfToken(raw, b),
Data: key, Data: key,
}) })
@@ -836,7 +866,7 @@ func (p *Parser) parseKey(b []byte) (reference, []byte, error) {
p.builder.PushAndChain(Node{ p.builder.PushAndChain(Node{
Kind: Key, Kind: Key,
Raw: p.Range(raw), Raw: p.rangeOfToken(raw, b),
Data: key, Data: key,
}) })
} else { } 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. // validate the string and return a direct reference to the buffer.
if !escaped { if !escaped {
str := token[startIdx:endIdx] str := token[startIdx:endIdx]
verr := characters.Utf8TomlValidAlreadyEscaped(str) highlight := characters.Utf8TomlValidAlreadyEscaped(str)
if verr.Zero() { if len(highlight) == 0 {
return token, str, rest, nil 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 i := startIdx
@@ -972,7 +1002,7 @@ func hexToRune(b []byte, length int) (rune, error) {
var r uint32 var r uint32
for i, c := range b { for i, c := range b {
d := uint32(0) var d uint32
switch { switch {
case '0' <= c && c <= '9': case '0' <= c && c <= '9':
d = uint32(c - '0') d = uint32(c - '0')
@@ -1013,7 +1043,7 @@ func (p *Parser) parseIntOrFloatOrDateTime(b []byte) (reference, []byte, error)
return p.builder.Push(Node{ return p.builder.Push(Node{
Kind: Float, Kind: Float,
Data: b[:3], Data: b[:3],
Raw: p.Range(b[:3]), Raw: p.rangeOfToken(b[:3], b[3:]),
}), b[3:], nil }), b[3:], nil
case 'n': case 'n':
if !scanFollowsNan(b) { if !scanFollowsNan(b) {
@@ -1023,7 +1053,7 @@ func (p *Parser) parseIntOrFloatOrDateTime(b []byte) (reference, []byte, error)
return p.builder.Push(Node{ return p.builder.Push(Node{
Kind: Float, Kind: Float,
Data: b[:3], Data: b[:3],
Raw: p.Range(b[:3]), Raw: p.rangeOfToken(b[:3], b[3:]),
}), b[3:], nil }), b[3:], nil
case '+', '-': case '+', '-':
return p.scanIntOrFloat(b) return p.scanIntOrFloat(b)
@@ -1076,7 +1106,7 @@ byteLoop:
} }
case c == 'T' || c == 't' || c == ':' || c == '.': case c == 'T' || c == 't' || c == ':' || c == '.':
hasTime = true hasTime = true
case c == '+' || c == '-' || c == 'Z' || c == 'z': case c == '+' || c == 'Z' || c == 'z':
hasTz = true hasTz = true
case c == ' ': case c == ' ':
if !seenSpace && i+1 < len(b) && isDigit(b[i+1]) { 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{ return p.builder.Push(Node{
Kind: Integer, Kind: Integer,
Data: b[:i], Data: b[:i],
Raw: p.Range(b[:i]), Raw: p.rangeOfToken(b[:i], b[i:]),
}), b[i:], nil }), b[i:], nil
} }
@@ -1172,7 +1202,7 @@ func (p *Parser) scanIntOrFloat(b []byte) (reference, []byte, error) {
return p.builder.Push(Node{ return p.builder.Push(Node{
Kind: Float, Kind: Float,
Data: b[:i+3], Data: b[:i+3],
Raw: p.Range(b[:i+3]), Raw: p.rangeOfToken(b[:i+3], b[i+3:]),
}), b[i+3:], nil }), b[i+3:], nil
} }
@@ -1184,7 +1214,7 @@ func (p *Parser) scanIntOrFloat(b []byte) (reference, []byte, error) {
return p.builder.Push(Node{ return p.builder.Push(Node{
Kind: Float, Kind: Float,
Data: b[:i+3], Data: b[:i+3],
Raw: p.Range(b[:i+3]), Raw: p.rangeOfToken(b[:i+3], b[i+3:]),
}), b[i+3:], nil }), b[i+3:], nil
} }
@@ -1207,7 +1237,7 @@ func (p *Parser) scanIntOrFloat(b []byte) (reference, []byte, error) {
return p.builder.Push(Node{ return p.builder.Push(Node{
Kind: kind, Kind: kind,
Data: b[:i], Data: b[:i],
Raw: p.Range(b[:i]), Raw: p.rangeOfToken(b[:i], b[i:]),
}), b[i:], nil }), b[i:], nil
} }
+28 -3
View File
@@ -1,7 +1,32 @@
package unstable package unstable
// The Unmarshaler interface may be implemented by types to customize their // Unmarshaler is implemented by types that can unmarshal a TOML
// behavior when being unmarshaled from a TOML document. // 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 { 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/
+23 -20
View File
@@ -1,32 +1,35 @@
run: version: "2"
timeout: 30m
linters: linters:
default: none
enable: enable:
- gofmt - depguard
- govet - govet
- goimports
- ineffassign - ineffassign
- misspell - misspell
- unused
- staticcheck - staticcheck
- typecheck - unused
disable-all: true settings:
depguard:
rules:
main:
deny:
- pkg: io/ioutil
desc: The io/ioutil package has been deprecated.
exclusions:
generated: lax
paths:
- .*\.pb\.go$
linters-settings: formatters:
depguard: enable:
rules: - gofmt
main: - goimports
deny: exclusions:
# The io/ioutil package has been deprecated. generated: lax
# https://go.dev/doc/go1.16#ioutil paths:
- pkg: "io/ioutil" - .*\.pb\.go$
desc: The io/ioutil package has been deprecated.
issues: issues:
exclude-files:
- ".*\\.pb\\.go$"
# show all
max-issues-per-linter: 0 max-issues-per-linter: 0
max-same-issues: 0 max-same-issues: 0
+54 -3
View File
@@ -1,11 +1,12 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG GO_VERSION=1.23 ARG GO_VERSION=1.26
ARG XX_VERSION=1.6.1 ARG ALPINE_VERSION=3.23
ARG XX_VERSION=1.9.0
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx 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 RUN apk add --no-cache git
COPY --from=xx / / COPY --from=xx / /
WORKDIR /src WORKDIR /src
@@ -35,4 +36,54 @@ COPY --from=test /tmp/coverage.txt /coverage-root.txt
FROM scratch AS test-noroot-coverage FROM scratch AS test-noroot-coverage
COPY --from=test-noroot /tmp/coverage.txt /coverage-noroot.txt 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 FROM build
+3
View File
@@ -10,3 +10,6 @@ coverage:
github_checks: github_checks:
annotations: false annotations: false
ignore:
- "**/*.pb.go"
+2 -3
View File
@@ -21,7 +21,7 @@ import (
const defaultDirectoryMode = 0755 const defaultDirectoryMode = 0755
var bufferPool = &sync.Pool{ var bufferPool = &sync.Pool{
New: func() interface{} { New: func() any {
buffer := make([]byte, 32*1024) buffer := make([]byte, 32*1024)
return &buffer return &buffer
}, },
@@ -738,8 +738,7 @@ func rel(basepath, targpath string) (string, error) {
// filepath.Rel can't handle UUID paths in windows // filepath.Rel can't handle UUID paths in windows
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
pfx := basepath + `\` pfx := basepath + `\`
if strings.HasPrefix(targpath, pfx) { if p, ok := strings.CutPrefix(targpath, pfx); ok {
p := strings.TrimPrefix(targpath, pfx)
if p == "" { if p == "" {
p = "." p = "."
} }
+66 -8
View File
@@ -3,7 +3,10 @@ package fsutil
import ( import (
"context" "context"
"hash" "hash"
gofs "io/fs"
"os" "os"
"path/filepath"
"runtime"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/tonistiigi/fsutil/types" "github.com/tonistiigi/fsutil/types"
@@ -11,26 +14,41 @@ import (
type walkerFn func(ctx context.Context, pathC chan<- *currentPath) error 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 HandleChangeFn func(ChangeKind, string, os.FileInfo, error) error
type ContentHasher func(*types.Stat) (hash.Hash, error) type ContentHasher func(*types.Stat) (hash.Hash, error)
func getWalkerFn(root string) walkerFn { 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 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 { if err != nil {
return err 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 { 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{ p := &currentPath{
path: path, path: path,
stat: stat, 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 { func emptyWalker(ctx context.Context, pathC chan<- *currentPath) error {
return nil return nil
} }
+22
View File
@@ -6,6 +6,10 @@ variable "DESTDIR" {
default = "./bin" default = "./bin"
} }
variable "BENCH_FILE_SIZE" {
default = null
}
target "_platforms" { target "_platforms" {
platforms = [ platforms = [
"darwin/amd64", "darwin/amd64",
@@ -60,6 +64,24 @@ target "test-noroot" {
output = ["${DESTDIR}/coverage"] 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" { group "lint" {
targets = ["lint-golangci", "lint-gopls"] 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() return ctx.Err()
default: default:
if fs.mapFn != nil { if fs.mapFn != nil {
result := fs.mapFn(stat.Path, stat) switch result := fs.mapFn(stat.Path, stat); result {
if result == MapResultSkipDir { case MapResultSkipDir:
return filepath.SkipDir return filepath.SkipDir
} else if result == MapResultExclude { case MapResultExclude:
return nil return nil
} }
} }
@@ -355,10 +355,10 @@ func (fs *filterFS) Walk(ctx context.Context, target string, fn gofs.WalkDirFunc
default: default:
} }
if fs.mapFn != nil { if fs.mapFn != nil {
result := fs.mapFn(parentStat.Path, parentStat) switch result := fs.mapFn(parentStat.Path, parentStat); result {
if result == MapResultExclude { case MapResultExclude:
continue continue
} else if result == MapResultSkipDir { case MapResultSkipDir:
parentDirs[i].skipFn = true parentDirs[i].skipFn = true
return filepath.SkipDir return filepath.SkipDir
} }
+71 -5
View File
@@ -40,6 +40,11 @@ func NewFS(root string) (FS, error) {
}, nil }, nil
} }
// NewRootFS creates a new FS from a filesystem root.
func NewRootFS(root Root) FS {
return &rootFS{root: root}
}
type fs struct { type fs struct {
root string root string
} }
@@ -90,6 +95,67 @@ func (fs *fs) Open(p string) (io.ReadCloser, error) {
return rc, errors.WithStack(err) 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 { type Dir struct {
Stat *types.Stat Stat *types.Stat
FS FS FS FS
@@ -187,7 +253,7 @@ type StatInfo struct {
} }
func (s *StatInfo) Name() string { func (s *StatInfo) Name() string {
return filepath.Base(s.Stat.Path) return filepath.Base(s.Path)
} }
func (s *StatInfo) Size() int64 { func (s *StatInfo) Size() int64 {
@@ -206,7 +272,7 @@ func (s *StatInfo) IsDir() bool {
return s.Mode().IsDir() return s.Mode().IsDir()
} }
func (s *StatInfo) Sys() interface{} { func (s *StatInfo) Sys() any {
return s.Stat return s.Stat
} }
@@ -221,7 +287,7 @@ type DirEntryInfo struct {
func (s *DirEntryInfo) Name() string { func (s *DirEntryInfo) Name() string {
if s.Stat != nil { if s.Stat != nil {
return filepath.Base(s.Stat.Path) return filepath.Base(s.Path)
} }
return s.entry.Name() return s.entry.Name()
} }
@@ -235,7 +301,7 @@ func (s *DirEntryInfo) IsDir() bool {
func (s *DirEntryInfo) Type() gofs.FileMode { func (s *DirEntryInfo) Type() gofs.FileMode {
if s.Stat != nil { if s.Stat != nil {
return gofs.FileMode(s.Stat.Mode) return gofs.FileMode(s.Mode)
} }
return s.entry.Type() return s.entry.Type()
} }
@@ -253,6 +319,6 @@ func (s *DirEntryInfo) Info() (gofs.FileInfo, error) {
s.Stat = stat s.Stat = stat
} }
st := s.Stat.Clone() st := s.Clone()
return &StatInfo{st}, nil return &StatInfo{st}, nil
} }
+61 -42
View File
@@ -6,45 +6,64 @@
Incremental file directory sync tools in golang. Incremental file directory sync tools in golang.
``` ```
BENCH_FILE_SIZE=10000 ./bench.test --test.bench . BENCH_FILE_SIZE=10000 docker buildx bake bench-root
BenchmarkCopyWithTar10-4 2000 995242 ns/op ...
BenchmarkCopyWithTar50-4 300 4710021 ns/op #17 0.303 + CGO_ENABLED=0 xx-go test -benchmem '-bench=.' '-run=^$' .
BenchmarkCopyWithTar200-4 100 16627260 ns/op #17 0.303 + tee /tmp/fsutil.log
BenchmarkCopyWithTar1000-4 20 60031459 ns/op #17 1.527 BenchmarkWalker/depth_1_target-32 28356 42258 ns/op 9234 B/op 174 allocs/op
BenchmarkCPA10-4 1000 1678367 ns/op #17 3.166 BenchmarkWalker/depth_1_doublestar_target-32 28647 42038 ns/op 9282 B/op 175 allocs/op
BenchmarkCPA50-4 500 3690306 ns/op #17 4.865 BenchmarkWalker/depth_2_star_target-32 1184 1009371 ns/op 200654 B/op 3971 allocs/op
BenchmarkCPA200-4 200 9495066 ns/op #17 6.342 BenchmarkWalker/depth_2_doublestar_target-32 1148 1007115 ns/op 195891 B/op 3908 allocs/op
BenchmarkCPA1000-4 50 29769289 ns/op #17 9.339 BenchmarkWalker/depth_3_star_star_target-32 39 28146516 ns/op 5221363 B/op 100915 allocs/op
BenchmarkDiffCopy10-4 2000 943889 ns/op #17 13.54 BenchmarkWalker/depth_3_doublestar_target-32 40 28496829 ns/op 5206464 B/op 100828 allocs/op
BenchmarkDiffCopy50-4 500 3285950 ns/op #17 17.99 BenchmarkWalker/depth_4_star_star_star_target-32 26 48224854 ns/op 6571213 B/op 119421 allocs/op
BenchmarkDiffCopy200-4 200 8563792 ns/op #17 25.32 BenchmarkWalker/depth_4_doublestar_target-32 24 45061931 ns/op 6488522 B/op 119315 allocs/op
BenchmarkDiffCopy1000-4 50 29511340 ns/op #17 28.67 BenchmarkWalker/depth_5_star_star_star_star_target-32 54 22124864 ns/op 2476377 B/op 42818 allocs/op
BenchmarkDiffCopyProto10-4 2000 944615 ns/op #17 32.59 BenchmarkWalker/depth_5_doublestar_target-32 49 21479412 ns/op 2460690 B/op 42699 allocs/op
BenchmarkDiffCopyProto50-4 500 3334940 ns/op #17 35.09 BenchmarkWalker/depth_6_star_star_star_star_star_target-32 28 38307776 ns/op 3998884 B/op 67772 allocs/op
BenchmarkDiffCopyProto200-4 200 9420038 ns/op #17 38.05 BenchmarkWalker/depth_6_doublestar_target-32 31 38242074 ns/op 3980841 B/op 67634 allocs/op
BenchmarkDiffCopyProto1000-4 50 30632429 ns/op #17 42.92 BenchmarkWalker/depth_6_doublestar_exclude_star_star_doublestar-32 2925 393602 ns/op 47439 B/op 1006 allocs/op
BenchmarkIncrementalDiffCopy10-4 2000 691993 ns/op #17 45.99 + cd bench
BenchmarkIncrementalDiffCopy50-4 1000 1304253 ns/op #17 45.99 + CGO_ENABLED=0 xx-go test -benchmem '-bench=.' '-run=^$' .
BenchmarkIncrementalDiffCopy200-4 500 3306519 ns/op #17 45.99 + tee /tmp/bench.log
BenchmarkIncrementalDiffCopy1000-4 200 10211343 ns/op #17 46.84 BenchmarkCopyWithTar10-32 283 4291776 ns/op 906824 B/op 843 allocs/op
BenchmarkIncrementalDiffCopy5000-4 20 55194427 ns/op #17 50.05 BenchmarkCopyWithTar50-32 50 24077499 ns/op 4999874 B/op 4514 allocs/op
BenchmarkIncrementalDiffCopy10000-4 20 91759289 ns/op #17 54.00 BenchmarkCopyWithTar200-32 15 74350687 ns/op 18757006 B/op 15347 allocs/op
BenchmarkIncrementalCopyWithTar10-4 2000 1020258 ns/op #17 56.31 BenchmarkCopyWithTar1000-32 5 259188166 ns/op 72393427 B/op 55045 allocs/op
BenchmarkIncrementalCopyWithTar50-4 300 5348786 ns/op #17 61.31 BenchmarkCPA10-32 339 3496169 ns/op 7102 B/op 77 allocs/op
BenchmarkIncrementalCopyWithTar200-4 100 19495000 ns/op #17 64.68 BenchmarkCPA50-32 74 15808000 ns/op 7102 B/op 77 allocs/op
BenchmarkIncrementalCopyWithTar1000-4 20 70338507 ns/op #17 67.26 BenchmarkCPA200-32 26 45892546 ns/op 7101 B/op 77 allocs/op
BenchmarkIncrementalRsync10-4 30 45215754 ns/op #17 70.08 BenchmarkCPA1000-32 8 147602854 ns/op 7103 B/op 77 allocs/op
BenchmarkIncrementalRsync50-4 30 45837260 ns/op #17 72.60 BenchmarkDiffCopy10-32 392 3045072 ns/op 219789 B/op 1123 allocs/op
BenchmarkIncrementalRsync200-4 30 48780614 ns/op #17 76.15 BenchmarkDiffCopy50-32 82 14024073 ns/op 1253831 B/op 5460 allocs/op
BenchmarkIncrementalRsync1000-4 20 54801892 ns/op #17 78.83 BenchmarkDiffCopy200-32 27 41259003 ns/op 4683591 B/op 18640 allocs/op
BenchmarkIncrementalRsync5000-4 20 84782542 ns/op #17 81.60 BenchmarkDiffCopy1000-32 8 125244042 ns/op 17285147 B/op 67649 allocs/op
BenchmarkIncrementalRsync10000-4 10 103355108 ns/op #17 83.90 BenchmarkDiffCopyProto10-32 412 2934103 ns/op 232184 B/op 1143 allocs/op
BenchmarkRsync10-4 30 46776470 ns/op #17 87.46 BenchmarkDiffCopyProto50-32 80 13809955 ns/op 1273311 B/op 5565 allocs/op
BenchmarkRsync50-4 30 48601555 ns/op #17 90.02 BenchmarkDiffCopyProto200-32 30 41169665 ns/op 4697476 B/op 18995 allocs/op
BenchmarkRsync200-4 20 59642691 ns/op #17 93.05 BenchmarkDiffCopyProto1000-32 8 127126319 ns/op 17334920 B/op 68883 allocs/op
BenchmarkRsync1000-4 20 101343010 ns/op #17 95.37 BenchmarkIncrementalDiffCopy10-32 1540 779568 ns/op 119068 B/op 1015 allocs/op
BenchmarkGnuTar10-4 500 3171448 ns/op #17 97.84 BenchmarkIncrementalDiffCopy50-32 782 1513121 ns/op 455740 B/op 4285 allocs/op
BenchmarkGnuTar50-4 300 5030296 ns/op #17 99.97 BenchmarkIncrementalDiffCopy200-32 271 4248983 ns/op 1329875 B/op 13603 allocs/op
BenchmarkGnuTar200-4 100 10464313 ns/op #17 102.3 BenchmarkIncrementalDiffCopy1000-32 84 14027390 ns/op 4552790 B/op 46849 allocs/op
BenchmarkGnuTar1000-4 50 30375257 ns/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 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 { func Receive(ctx context.Context, conn Stream, dest string, opt ReceiveOpt) error {
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
defer cancel() 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}, conn: &syncStream{Stream: conn},
dest: dest,
files: make(map[string]uint32), files: make(map[string]uint32),
pipes: make(map[uint32]io.WriteCloser), pipes: make(map[uint32]io.WriteCloser),
notifyHashed: opt.NotifyHashed, notifyHashed: opt.NotifyHashed,
@@ -81,11 +100,11 @@ func Receive(ctx context.Context, conn Stream, dest string, opt ReceiveOpt) erro
differ: opt.Differ, differ: opt.Differ,
metadataOnly: opt.MetadataOnly, metadataOnly: opt.MetadataOnly,
} }
return r.run(ctx)
} }
type receiver struct { type receiver struct {
dest string dest string
root Root
conn Stream conn Stream
files map[string]uint32 files map[string]uint32
pipes map[uint32]io.WriteCloser 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 { func (r *receiver) run(ctx context.Context) error {
g, ctx := errgroup.WithContext(ctx) g, ctx := errgroup.WithContext(ctx)
dw, err := NewDiskWriter(ctx, r.dest, DiskWriterOpt{ dw, err := r.newDiskWriter(ctx, DiskWriterOpt{
AsyncDataCb: r.asyncDataFunc, AsyncDataCb: r.asyncDataFunc,
NotifyCb: r.notifyHashed, NotifyCb: r.notifyHashed,
ContentHasher: r.contentHasher, ContentHasher: r.contentHasher,
@@ -179,12 +198,21 @@ func (r *receiver) run(ctx context.Context) error {
g.Go(func() (retErr error) { g.Go(func() (retErr error) {
defer func() { defer func() {
if retErr != nil { 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())}) r.conn.SendMsg(&types.Packet{Type: types.PACKET_ERR, Data: []byte(retErr.Error())})
} }
}() }()
destWalker := emptyWalker destWalker := emptyWalker
if !r.merge { if !r.merge {
destWalker = getWalkerFn(r.dest) destWalker = r.destWalker()
} }
err := doubleWalkDiff(ctx, dw.HandleChange, destWalker, w.fill, r.filter, r.differ) err := doubleWalkDiff(ctx, dw.HandleChange, destWalker, w.fill, r.filter, r.differ)
if err != nil { if err != nil {
@@ -333,12 +361,45 @@ func (r *receiver) run(ctx context.Context) error {
return nil 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 // although we don't allow tranferring metadataPath, make sure there was no preexisting file/symlink
os.Remove(filepath.Join(r.dest, metadataPath)) 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) f, err := os.OpenFile(filepath.Join(r.dest, metadataPath), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil { if err != nil {
return err return errors.WithStack(err)
} }
if _, err := metadataBuffer.WriteTo(f); err != nil { if _, err := metadataBuffer.WriteTo(f); err != nil {
f.Close() 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
}
+13
View File
@@ -0,0 +1,13 @@
//go:build windows
package fsutil
import (
"time"
"github.com/tonistiigi/fsutil/types"
)
func rewriteRootMetadata(root Root, p string, stat *types.Stat) error {
return root.LChtimes(p, time.Unix(0, stat.ModTime))
}
+84
View File
@@ -0,0 +1,84 @@
package fsutil
import (
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
)
type rootStack struct {
items []rootStackItem
}
type rootStackItem struct {
path string
root Root
}
func newRootStack(root Root) *rootStack {
return &rootStack{
items: []rootStackItem{{path: ".", root: root}},
}
}
func (s *rootStack) get(path string) (Root, string, error) {
path = cleanRootPath(path)
dir := filepath.Dir(path)
base := filepath.Base(path)
for {
top := s.items[len(s.items)-1]
if top.path == dir {
return top.root, base, nil
}
if rel, ok := rootStackRel(top.path, dir); ok {
osroot, err := top.root.OpenRoot(rel)
if err != nil {
return nil, "", errors.WithStack(err)
}
root := NewRoot(osroot)
s.items = append(s.items, rootStackItem{path: dir, root: root})
return root, base, nil
}
if len(s.items) == 1 {
return nil, "", errors.WithStack(&os.PathError{Op: "openroot", Path: dir, Err: os.ErrNotExist})
}
if err := s.pop(); err != nil {
return nil, "", err
}
}
}
func (s *rootStack) Close() error {
var err error
for len(s.items) > 1 {
if err2 := s.pop(); err == nil {
err = err2
}
}
return err
}
func (s *rootStack) pop() error {
i := len(s.items) - 1
item := s.items[i]
s.items = s.items[:i]
return item.root.Close()
}
// rootStackRel returns target relative to base when target is below base.
func rootStackRel(base, target string) (string, bool) {
if base == "." {
if target == "." {
return "", false
}
return target, true
}
rel, err := filepath.Rel(base, target)
if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "", false
}
return rel, true
}
+5 -5
View File
@@ -14,15 +14,15 @@ import (
) )
var bufPool = sync.Pool{ var bufPool = sync.Pool{
New: func() interface{} { New: func() any {
buf := make([]byte, 32*1<<10) buf := make([]byte, 32*1<<10)
return &buf return &buf
}, },
} }
type Stream interface { type Stream interface {
RecvMsg(interface{}) error RecvMsg(any) error
SendMsg(m interface{}) error SendMsg(m any) error
Context() context.Context Context() context.Context
} }
@@ -66,7 +66,7 @@ func (s *sender) run(ctx context.Context) error {
return err return err
}) })
for i := 0; i < 4; i++ { for range 4 {
g.Go(func() error { g.Go(func() error {
for h := range s.sendpipeline { for h := range s.sendpipeline {
select { select {
@@ -210,7 +210,7 @@ type syncStream struct {
mu sync.Mutex mu sync.Mutex
} }
func (ss *syncStream) SendMsg(m interface{}) error { func (ss *syncStream) SendMsg(m any) error {
ss.mu.Lock() ss.mu.Lock()
err := ss.Stream.SendMsg(m) err := ss.Stream.SendMsg(m)
ss.mu.Unlock() ss.mu.Unlock()
+1 -4
View File
@@ -43,10 +43,7 @@ func loadXattr(origpath string, stat *types.Stat) error {
} }
func skipXattr(key string) bool { func skipXattr(key string) bool {
if strings.HasPrefix(key, xattrApplePrefix) { return strings.HasPrefix(key, xattrApplePrefix)
return true
}
return false
} }
func setUnixOpt(fi os.FileInfo, stat *types.Stat, path string, seenFiles map[uint64]string) { func setUnixOpt(fi os.FileInfo, stat *types.Stat, path string, seenFiles map[uint64]string) {
+41 -73
View File
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.31.0 // protoc-gen-go v1.36.11
// protoc v3.11.4 // protoc v3.11.4
// source: github.com/tonistiigi/fsutil/types/stat.proto // source: github.com/tonistiigi/fsutil/types/stat.proto
@@ -12,6 +12,7 @@ import (
protoimpl "google.golang.org/protobuf/runtime/protoimpl" protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect" reflect "reflect"
sync "sync" sync "sync"
unsafe "unsafe"
) )
const ( const (
@@ -22,30 +23,27 @@ const (
) )
type Stat struct { type Stat struct {
state protoimpl.MessageState state protoimpl.MessageState `protogen:"open.v1"`
sizeCache protoimpl.SizeCache Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
unknownFields protoimpl.UnknownFields Mode uint32 `protobuf:"varint,2,opt,name=mode,proto3" json:"mode,omitempty"`
Uid uint32 `protobuf:"varint,3,opt,name=uid,proto3" json:"uid,omitempty"`
Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` Gid uint32 `protobuf:"varint,4,opt,name=gid,proto3" json:"gid,omitempty"`
Mode uint32 `protobuf:"varint,2,opt,name=mode,proto3" json:"mode,omitempty"` Size int64 `protobuf:"varint,5,opt,name=size,proto3" json:"size,omitempty"`
Uid uint32 `protobuf:"varint,3,opt,name=uid,proto3" json:"uid,omitempty"` ModTime int64 `protobuf:"varint,6,opt,name=modTime,proto3" json:"modTime,omitempty"`
Gid uint32 `protobuf:"varint,4,opt,name=gid,proto3" json:"gid,omitempty"`
Size int64 `protobuf:"varint,5,opt,name=size,proto3" json:"size,omitempty"`
ModTime int64 `protobuf:"varint,6,opt,name=modTime,proto3" json:"modTime,omitempty"`
// int32 typeflag = 7; // int32 typeflag = 7;
Linkname string `protobuf:"bytes,7,opt,name=linkname,proto3" json:"linkname,omitempty"` Linkname string `protobuf:"bytes,7,opt,name=linkname,proto3" json:"linkname,omitempty"`
Devmajor int64 `protobuf:"varint,8,opt,name=devmajor,proto3" json:"devmajor,omitempty"` Devmajor int64 `protobuf:"varint,8,opt,name=devmajor,proto3" json:"devmajor,omitempty"`
Devminor int64 `protobuf:"varint,9,opt,name=devminor,proto3" json:"devminor,omitempty"` Devminor int64 `protobuf:"varint,9,opt,name=devminor,proto3" json:"devminor,omitempty"`
Xattrs map[string][]byte `protobuf:"bytes,10,rep,name=xattrs,proto3" json:"xattrs,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` Xattrs map[string][]byte `protobuf:"bytes,10,rep,name=xattrs,proto3" json:"xattrs,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
} }
func (x *Stat) Reset() { func (x *Stat) Reset() {
*x = Stat{} *x = Stat{}
if protoimpl.UnsafeEnabled { mi := &file_github_com_tonistiigi_fsutil_types_stat_proto_msgTypes[0]
mi := &file_github_com_tonistiigi_fsutil_types_stat_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *Stat) String() string { func (x *Stat) String() string {
@@ -56,7 +54,7 @@ func (*Stat) ProtoMessage() {}
func (x *Stat) ProtoReflect() protoreflect.Message { func (x *Stat) ProtoReflect() protoreflect.Message {
mi := &file_github_com_tonistiigi_fsutil_types_stat_proto_msgTypes[0] mi := &file_github_com_tonistiigi_fsutil_types_stat_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@@ -143,54 +141,39 @@ func (x *Stat) GetXattrs() map[string][]byte {
var File_github_com_tonistiigi_fsutil_types_stat_proto protoreflect.FileDescriptor var File_github_com_tonistiigi_fsutil_types_stat_proto protoreflect.FileDescriptor
var file_github_com_tonistiigi_fsutil_types_stat_proto_rawDesc = []byte{ const file_github_com_tonistiigi_fsutil_types_stat_proto_rawDesc = "" +
0x0a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x6f, 0x6e, "\n" +
0x69, 0x73, 0x74, 0x69, 0x69, 0x67, 0x69, 0x2f, 0x66, 0x73, 0x75, 0x74, 0x69, 0x6c, 0x2f, 0x74, "-github.com/tonistiigi/fsutil/types/stat.proto\x12\ffsutil.types\x1a3github.com/planetscale/vtprotobuf/vtproto/ext.proto\"\xc7\x02\n" +
0x79, 0x70, 0x65, 0x73, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, "\x04Stat\x12\x12\n" +
0x0c, 0x66, 0x73, 0x75, 0x74, 0x69, 0x6c, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x1a, 0x33, 0x67, "\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" +
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x74, "\x04mode\x18\x02 \x01(\rR\x04mode\x12\x10\n" +
0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, "\x03uid\x18\x03 \x01(\rR\x03uid\x12\x10\n" +
0x2f, 0x76, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x78, 0x74, 0x2e, 0x70, 0x72, 0x6f, "\x03gid\x18\x04 \x01(\rR\x03gid\x12\x12\n" +
0x74, 0x6f, 0x22, 0xc7, 0x02, 0x0a, 0x04, 0x53, 0x74, 0x61, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, "\x04size\x18\x05 \x01(\x03R\x04size\x12\x18\n" +
0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, "\amodTime\x18\x06 \x01(\x03R\amodTime\x12\x1a\n" +
0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x6d, "\blinkname\x18\a \x01(\tR\blinkname\x12\x1a\n" +
0x6f, 0x64, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, "\bdevmajor\x18\b \x01(\x03R\bdevmajor\x12\x1a\n" +
0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, "\bdevminor\x18\t \x01(\x03R\bdevminor\x126\n" +
0x28, 0x0d, 0x52, 0x03, 0x67, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, "\x06xattrs\x18\n" +
0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, " \x03(\v2\x1e.fsutil.types.Stat.XattrsEntryR\x06xattrs\x1a9\n" +
0x6f, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x6d, 0x6f, "\vXattrsEntry\x12\x10\n" +
0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x69, 0x6e, 0x6b, 0x6e, 0x61, 0x6d, "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x69, 0x6e, 0x6b, 0x6e, 0x61, 0x6d, "\x05value\x18\x02 \x01(\fR\x05value:\x028\x01B$Z\"github.com/tonistiigi/fsutil/typesb\x06proto3"
0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x65, 0x76, 0x6d, 0x61, 0x6a, 0x6f, 0x72, 0x18, 0x08, 0x20,
0x01, 0x28, 0x03, 0x52, 0x08, 0x64, 0x65, 0x76, 0x6d, 0x61, 0x6a, 0x6f, 0x72, 0x12, 0x1a, 0x0a,
0x08, 0x64, 0x65, 0x76, 0x6d, 0x69, 0x6e, 0x6f, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52,
0x08, 0x64, 0x65, 0x76, 0x6d, 0x69, 0x6e, 0x6f, 0x72, 0x12, 0x36, 0x0a, 0x06, 0x78, 0x61, 0x74,
0x74, 0x72, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x66, 0x73, 0x75, 0x74,
0x69, 0x6c, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x2e, 0x58, 0x61,
0x74, 0x74, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x78, 0x61, 0x74, 0x74, 0x72,
0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x58, 0x61, 0x74, 0x74, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79,
0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b,
0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x24, 0x5a, 0x22,
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x6f, 0x6e, 0x69, 0x73,
0x74, 0x69, 0x69, 0x67, 0x69, 0x2f, 0x66, 0x73, 0x75, 0x74, 0x69, 0x6c, 0x2f, 0x74, 0x79, 0x70,
0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var ( var (
file_github_com_tonistiigi_fsutil_types_stat_proto_rawDescOnce sync.Once file_github_com_tonistiigi_fsutil_types_stat_proto_rawDescOnce sync.Once
file_github_com_tonistiigi_fsutil_types_stat_proto_rawDescData = file_github_com_tonistiigi_fsutil_types_stat_proto_rawDesc file_github_com_tonistiigi_fsutil_types_stat_proto_rawDescData []byte
) )
func file_github_com_tonistiigi_fsutil_types_stat_proto_rawDescGZIP() []byte { func file_github_com_tonistiigi_fsutil_types_stat_proto_rawDescGZIP() []byte {
file_github_com_tonistiigi_fsutil_types_stat_proto_rawDescOnce.Do(func() { file_github_com_tonistiigi_fsutil_types_stat_proto_rawDescOnce.Do(func() {
file_github_com_tonistiigi_fsutil_types_stat_proto_rawDescData = protoimpl.X.CompressGZIP(file_github_com_tonistiigi_fsutil_types_stat_proto_rawDescData) file_github_com_tonistiigi_fsutil_types_stat_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_github_com_tonistiigi_fsutil_types_stat_proto_rawDesc), len(file_github_com_tonistiigi_fsutil_types_stat_proto_rawDesc)))
}) })
return file_github_com_tonistiigi_fsutil_types_stat_proto_rawDescData return file_github_com_tonistiigi_fsutil_types_stat_proto_rawDescData
} }
var file_github_com_tonistiigi_fsutil_types_stat_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_github_com_tonistiigi_fsutil_types_stat_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_github_com_tonistiigi_fsutil_types_stat_proto_goTypes = []interface{}{ var file_github_com_tonistiigi_fsutil_types_stat_proto_goTypes = []any{
(*Stat)(nil), // 0: fsutil.types.Stat (*Stat)(nil), // 0: fsutil.types.Stat
nil, // 1: fsutil.types.Stat.XattrsEntry nil, // 1: fsutil.types.Stat.XattrsEntry
} }
@@ -208,25 +191,11 @@ func file_github_com_tonistiigi_fsutil_types_stat_proto_init() {
if File_github_com_tonistiigi_fsutil_types_stat_proto != nil { if File_github_com_tonistiigi_fsutil_types_stat_proto != nil {
return return
} }
if !protoimpl.UnsafeEnabled {
file_github_com_tonistiigi_fsutil_types_stat_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Stat); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_github_com_tonistiigi_fsutil_types_stat_proto_rawDesc, RawDescriptor: unsafe.Slice(unsafe.StringData(file_github_com_tonistiigi_fsutil_types_stat_proto_rawDesc), len(file_github_com_tonistiigi_fsutil_types_stat_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 2, NumMessages: 2,
NumExtensions: 0, NumExtensions: 0,
@@ -237,7 +206,6 @@ func file_github_com_tonistiigi_fsutil_types_stat_proto_init() {
MessageInfos: file_github_com_tonistiigi_fsutil_types_stat_proto_msgTypes, MessageInfos: file_github_com_tonistiigi_fsutil_types_stat_proto_msgTypes,
}.Build() }.Build()
File_github_com_tonistiigi_fsutil_types_stat_proto = out.File File_github_com_tonistiigi_fsutil_types_stat_proto = out.File
file_github_com_tonistiigi_fsutil_types_stat_proto_rawDesc = nil
file_github_com_tonistiigi_fsutil_types_stat_proto_goTypes = nil file_github_com_tonistiigi_fsutil_types_stat_proto_goTypes = nil
file_github_com_tonistiigi_fsutil_types_stat_proto_depIdxs = nil file_github_com_tonistiigi_fsutil_types_stat_proto_depIdxs = nil
} }
+34 -64
View File
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.31.0 // protoc-gen-go v1.36.11
// protoc v3.11.4 // protoc v3.11.4
// source: github.com/tonistiigi/fsutil/types/wire.proto // source: github.com/tonistiigi/fsutil/types/wire.proto
@@ -12,6 +12,7 @@ import (
protoimpl "google.golang.org/protobuf/runtime/protoimpl" protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect" reflect "reflect"
sync "sync" sync "sync"
unsafe "unsafe"
) )
const ( const (
@@ -77,23 +78,20 @@ func (Packet_PacketType) EnumDescriptor() ([]byte, []int) {
} }
type Packet struct { type Packet struct {
state protoimpl.MessageState state protoimpl.MessageState `protogen:"open.v1"`
sizeCache protoimpl.SizeCache Type Packet_PacketType `protobuf:"varint,1,opt,name=type,proto3,enum=fsutil.types.Packet_PacketType" json:"type,omitempty"`
Stat *Stat `protobuf:"bytes,2,opt,name=stat,proto3" json:"stat,omitempty"`
ID uint32 `protobuf:"varint,3,opt,name=ID,proto3" json:"ID,omitempty"`
Data []byte `protobuf:"bytes,4,opt,name=data,proto3" json:"data,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
Type Packet_PacketType `protobuf:"varint,1,opt,name=type,proto3,enum=fsutil.types.Packet_PacketType" json:"type,omitempty"`
Stat *Stat `protobuf:"bytes,2,opt,name=stat,proto3" json:"stat,omitempty"`
ID uint32 `protobuf:"varint,3,opt,name=ID,proto3" json:"ID,omitempty"`
Data []byte `protobuf:"bytes,4,opt,name=data,proto3" json:"data,omitempty"`
} }
func (x *Packet) Reset() { func (x *Packet) Reset() {
*x = Packet{} *x = Packet{}
if protoimpl.UnsafeEnabled { mi := &file_github_com_tonistiigi_fsutil_types_wire_proto_msgTypes[0]
mi := &file_github_com_tonistiigi_fsutil_types_wire_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *Packet) String() string { func (x *Packet) String() string {
@@ -104,7 +102,7 @@ func (*Packet) ProtoMessage() {}
func (x *Packet) ProtoReflect() protoreflect.Message { func (x *Packet) ProtoReflect() protoreflect.Message {
mi := &file_github_com_tonistiigi_fsutil_types_wire_proto_msgTypes[0] mi := &file_github_com_tonistiigi_fsutil_types_wire_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@@ -149,53 +147,40 @@ func (x *Packet) GetData() []byte {
var File_github_com_tonistiigi_fsutil_types_wire_proto protoreflect.FileDescriptor var File_github_com_tonistiigi_fsutil_types_wire_proto protoreflect.FileDescriptor
var file_github_com_tonistiigi_fsutil_types_wire_proto_rawDesc = []byte{ const file_github_com_tonistiigi_fsutil_types_wire_proto_rawDesc = "" +
0x0a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x6f, 0x6e, "\n" +
0x69, 0x73, 0x74, 0x69, 0x69, 0x67, 0x69, 0x2f, 0x66, 0x73, 0x75, 0x74, 0x69, 0x6c, 0x2f, 0x74, "-github.com/tonistiigi/fsutil/types/wire.proto\x12\ffsutil.types\x1a3github.com/planetscale/vtprotobuf/vtproto/ext.proto\x1a-github.com/tonistiigi/fsutil/types/stat.proto\"\xef\x01\n" +
0x79, 0x70, 0x65, 0x73, 0x2f, 0x77, 0x69, 0x72, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, "\x06Packet\x123\n" +
0x0c, 0x66, 0x73, 0x75, 0x74, 0x69, 0x6c, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x1a, 0x33, 0x67, "\x04type\x18\x01 \x01(\x0e2\x1f.fsutil.types.Packet.PacketTypeR\x04type\x12&\n" +
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x74, "\x04stat\x18\x02 \x01(\v2\x12.fsutil.types.StatR\x04stat\x12\x0e\n" +
0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, "\x02ID\x18\x03 \x01(\rR\x02ID\x12\x12\n" +
0x2f, 0x76, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x78, 0x74, 0x2e, 0x70, 0x72, 0x6f, "\x04data\x18\x04 \x01(\fR\x04data\"^\n" +
0x74, 0x6f, 0x1a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, "\n" +
0x6f, 0x6e, 0x69, 0x73, 0x74, 0x69, 0x69, 0x67, 0x69, 0x2f, 0x66, 0x73, 0x75, 0x74, 0x69, 0x6c, "PacketType\x12\x0f\n" +
0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, "\vPACKET_STAT\x10\x00\x12\x0e\n" +
0x6f, 0x22, 0xef, 0x01, 0x0a, 0x06, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x33, 0x0a, 0x04, "\n" +
0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x66, 0x73, 0x75, "PACKET_REQ\x10\x01\x12\x0f\n" +
0x74, 0x69, 0x6c, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, "\vPACKET_DATA\x10\x02\x12\x0e\n" +
0x2e, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, "\n" +
0x65, 0x12, 0x26, 0x0a, 0x04, 0x73, 0x74, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, "PACKET_FIN\x10\x03\x12\x0e\n" +
0x12, 0x2e, 0x66, 0x73, 0x75, 0x74, 0x69, 0x6c, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, "\n" +
0x74, 0x61, 0x74, 0x52, 0x04, 0x73, 0x74, 0x61, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, "PACKET_ERR\x10\x04:\x04\xa8\xa6\x1f\x01B$Z\"github.com/tonistiigi/fsutil/typesb\x06proto3"
0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74,
0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x5e, 0x0a,
0x0a, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x50,
0x41, 0x43, 0x4b, 0x45, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x10, 0x00, 0x12, 0x0e, 0x0a, 0x0a,
0x50, 0x41, 0x43, 0x4b, 0x45, 0x54, 0x5f, 0x52, 0x45, 0x51, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b,
0x50, 0x41, 0x43, 0x4b, 0x45, 0x54, 0x5f, 0x44, 0x41, 0x54, 0x41, 0x10, 0x02, 0x12, 0x0e, 0x0a,
0x0a, 0x50, 0x41, 0x43, 0x4b, 0x45, 0x54, 0x5f, 0x46, 0x49, 0x4e, 0x10, 0x03, 0x12, 0x0e, 0x0a,
0x0a, 0x50, 0x41, 0x43, 0x4b, 0x45, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x10, 0x04, 0x3a, 0x04, 0xa8,
0xa6, 0x1f, 0x01, 0x42, 0x24, 0x5a, 0x22, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
0x6d, 0x2f, 0x74, 0x6f, 0x6e, 0x69, 0x73, 0x74, 0x69, 0x69, 0x67, 0x69, 0x2f, 0x66, 0x73, 0x75,
0x74, 0x69, 0x6c, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x33,
}
var ( var (
file_github_com_tonistiigi_fsutil_types_wire_proto_rawDescOnce sync.Once file_github_com_tonistiigi_fsutil_types_wire_proto_rawDescOnce sync.Once
file_github_com_tonistiigi_fsutil_types_wire_proto_rawDescData = file_github_com_tonistiigi_fsutil_types_wire_proto_rawDesc file_github_com_tonistiigi_fsutil_types_wire_proto_rawDescData []byte
) )
func file_github_com_tonistiigi_fsutil_types_wire_proto_rawDescGZIP() []byte { func file_github_com_tonistiigi_fsutil_types_wire_proto_rawDescGZIP() []byte {
file_github_com_tonistiigi_fsutil_types_wire_proto_rawDescOnce.Do(func() { file_github_com_tonistiigi_fsutil_types_wire_proto_rawDescOnce.Do(func() {
file_github_com_tonistiigi_fsutil_types_wire_proto_rawDescData = protoimpl.X.CompressGZIP(file_github_com_tonistiigi_fsutil_types_wire_proto_rawDescData) file_github_com_tonistiigi_fsutil_types_wire_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_github_com_tonistiigi_fsutil_types_wire_proto_rawDesc), len(file_github_com_tonistiigi_fsutil_types_wire_proto_rawDesc)))
}) })
return file_github_com_tonistiigi_fsutil_types_wire_proto_rawDescData return file_github_com_tonistiigi_fsutil_types_wire_proto_rawDescData
} }
var file_github_com_tonistiigi_fsutil_types_wire_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_github_com_tonistiigi_fsutil_types_wire_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_github_com_tonistiigi_fsutil_types_wire_proto_msgTypes = make([]protoimpl.MessageInfo, 1) var file_github_com_tonistiigi_fsutil_types_wire_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_github_com_tonistiigi_fsutil_types_wire_proto_goTypes = []interface{}{ var file_github_com_tonistiigi_fsutil_types_wire_proto_goTypes = []any{
(Packet_PacketType)(0), // 0: fsutil.types.Packet.PacketType (Packet_PacketType)(0), // 0: fsutil.types.Packet.PacketType
(*Packet)(nil), // 1: fsutil.types.Packet (*Packet)(nil), // 1: fsutil.types.Packet
(*Stat)(nil), // 2: fsutil.types.Stat (*Stat)(nil), // 2: fsutil.types.Stat
@@ -216,25 +201,11 @@ func file_github_com_tonistiigi_fsutil_types_wire_proto_init() {
return return
} }
file_github_com_tonistiigi_fsutil_types_stat_proto_init() file_github_com_tonistiigi_fsutil_types_stat_proto_init()
if !protoimpl.UnsafeEnabled {
file_github_com_tonistiigi_fsutil_types_wire_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Packet); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_github_com_tonistiigi_fsutil_types_wire_proto_rawDesc, RawDescriptor: unsafe.Slice(unsafe.StringData(file_github_com_tonistiigi_fsutil_types_wire_proto_rawDesc), len(file_github_com_tonistiigi_fsutil_types_wire_proto_rawDesc)),
NumEnums: 1, NumEnums: 1,
NumMessages: 1, NumMessages: 1,
NumExtensions: 0, NumExtensions: 0,
@@ -246,7 +217,6 @@ func file_github_com_tonistiigi_fsutil_types_wire_proto_init() {
MessageInfos: file_github_com_tonistiigi_fsutil_types_wire_proto_msgTypes, MessageInfos: file_github_com_tonistiigi_fsutil_types_wire_proto_msgTypes,
}.Build() }.Build()
File_github_com_tonistiigi_fsutil_types_wire_proto = out.File File_github_com_tonistiigi_fsutil_types_wire_proto = out.File
file_github_com_tonistiigi_fsutil_types_wire_proto_rawDesc = nil
file_github_com_tonistiigi_fsutil_types_wire_proto_goTypes = nil file_github_com_tonistiigi_fsutil_types_wire_proto_goTypes = nil
file_github_com_tonistiigi_fsutil_types_wire_proto_depIdxs = nil file_github_com_tonistiigi_fsutil_types_wire_proto_depIdxs = nil
} }
+2 -9
View File
@@ -67,8 +67,8 @@ func (v *Validator) HandleChange(kind ChangeKind, p string, fi os.FileInfo, err
func ComparePath(p1, p2 string) int { func ComparePath(p1, p2 string) int {
// byte-by-byte comparison to be compatible with str<>str // byte-by-byte comparison to be compatible with str<>str
min := min(len(p1), len(p2)) n := min(len(p1), len(p2))
for i := 0; i < min; i++ { for i := range n {
switch { switch {
case p1[i] == p2[i]: case p1[i] == p2[i]:
continue continue
@@ -80,10 +80,3 @@ func ComparePath(p1, p2 string) int {
} }
return len(p1) - len(p2) return len(p1) - len(p2)
} }
func min(x, y int) int {
if x < y {
return x
}
return y
}
@@ -47,7 +47,6 @@ type config struct {
TracerProvider trace.TracerProvider TracerProvider trace.TracerProvider
MeterProvider metric.MeterProvider MeterProvider metric.MeterProvider
SpanKind trace.SpanKind SpanKind trace.SpanKind
SpanStartOptions []trace.SpanStartOption
SpanAttributes []attribute.KeyValue SpanAttributes []attribute.KeyValue
MetricAttributes []attribute.KeyValue MetricAttributes []attribute.KeyValue
MetricAttributesFn func(ctx context.Context) []attribute.KeyValue MetricAttributesFn func(ctx context.Context) []attribute.KeyValue
@@ -204,16 +203,6 @@ func WithMessageEvents(events ...Event) Option {
}) })
} }
// WithSpanOptions configures an additional set of
// trace.SpanOptions, which are applied to each new span.
//
// Deprecated: It is only used by the deprecated interceptor, and is unused by [NewClientHandler] and [NewServerHandler].
func WithSpanOptions(opts ...trace.SpanStartOption) Option {
return optionFunc(func(c *config) {
c.SpanStartOptions = append(c.SpanStartOptions, opts...)
})
}
// WithSpanKind returns an Option to set the span kind for spans created by // WithSpanKind returns an Option to set the span kind for spans created by
// the handler. // the handler.
// //
@@ -11,7 +11,7 @@ import (
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0" semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
grpc_codes "google.golang.org/grpc/codes" grpc_codes "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
) )
@@ -9,7 +9,7 @@ import (
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
oldsemconv "go.opentelemetry.io/otel/semconv/v1.37.0" //nolint:depguard // Use of v1.37.0 is required for backward compatibility stability opt-in. oldsemconv "go.opentelemetry.io/otel/semconv/v1.37.0" //nolint:depguard // Use of v1.37.0 is required for backward compatibility stability opt-in.
semconv "go.opentelemetry.io/otel/semconv/v1.40.0" semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
) )
// ParseFullMethod returns a span name following the OpenTelemetry semantic // ParseFullMethod returns a span name following the OpenTelemetry semantic
@@ -13,8 +13,8 @@ import (
"go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/metric"
oldrpcconv "go.opentelemetry.io/otel/semconv/v1.37.0/rpcconv" //nolint:depguard // Use of v1.37.0 is required for backward compatibility stability opt-in. oldrpcconv "go.opentelemetry.io/otel/semconv/v1.37.0/rpcconv" //nolint:depguard // Use of v1.37.0 is required for backward compatibility stability opt-in.
semconv "go.opentelemetry.io/otel/semconv/v1.40.0" semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
"go.opentelemetry.io/otel/semconv/v1.40.0/rpcconv" "go.opentelemetry.io/otel/semconv/v1.41.0/rpcconv"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
grpc_codes "google.golang.org/grpc/codes" grpc_codes "google.golang.org/grpc/codes"
@@ -367,27 +367,39 @@ func (*config) handleRPC(
span.End() span.End()
} }
var metricAttrs []attribute.KeyValue var durationEnabled bool
if gctx != nil { var oldDurationEnabled bool
// Don't use gctx.metricAttrSet here, because it requires passing
// multiple RecordOptions, which would call metric.mergeSets and
// allocate a new set for each Record call.
metricAttrs = make([]attribute.KeyValue, 0, len(gctx.metricAttrs)+1)
metricAttrs = append(metricAttrs, gctx.metricAttrs...)
}
metricAttrs = append(metricAttrs, rpcStatusAttr)
// Allocate vararg slice once.
recordOpts := []metric.RecordOption{metric.WithAttributeSet(attribute.NewSet(metricAttrs...))}
// Use floating point division here for higher precision (instead of Millisecond method).
// Measure right before calling Record() to capture as much elapsed time as possible.
elapsedTime := float64(rs.EndTime.Sub(rs.BeginTime)) / float64(time.Second)
if duration != nil { if duration != nil {
duration.Record(ctx, elapsedTime, recordOpts...) durationEnabled = duration.Enabled(ctx)
} }
if oldDuration != nil { if oldDuration != nil {
oldDuration.Record(ctx, elapsedTime*1000.0, recordOpts...) oldDurationEnabled = oldDuration.Enabled(ctx)
}
if durationEnabled || oldDurationEnabled {
var metricAttrs []attribute.KeyValue
if gctx != nil {
// Don't use gctx.metricAttrSet here, because it requires passing
// multiple RecordOptions, which would call metric.mergeSets and
// allocate a new set for each Record call.
metricAttrs = make([]attribute.KeyValue, 0, len(gctx.metricAttrs)+1)
metricAttrs = append(metricAttrs, gctx.metricAttrs...)
}
metricAttrs = append(metricAttrs, rpcStatusAttr)
// Allocate vararg slice once.
recordOpts := []metric.RecordOption{metric.WithAttributeSet(attribute.NewSet(metricAttrs...))}
// Use floating point division here for higher precision (instead of Millisecond method).
// Measure right before calling Record() to capture as much elapsed time as possible.
elapsedTime := float64(rs.EndTime.Sub(rs.BeginTime)) / float64(time.Second)
if durationEnabled {
duration.Record(ctx, elapsedTime, recordOpts...)
}
if oldDurationEnabled {
oldDuration.Record(ctx, elapsedTime*1000.0, recordOpts...)
}
} }
default: default:
@@ -4,4 +4,4 @@
package otelgrpc // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" package otelgrpc // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
// Version is the current release version of the gRPC instrumentation. // Version is the current release version of the gRPC instrumentation.
const Version = "0.68.0" const Version = "0.69.0"
@@ -63,8 +63,20 @@ func (fn clientTraceOptionFunc) apply(c *clientTracer) {
} }
// WithoutSubSpans will modify the httptrace.ClientTrace to only collect data // WithoutSubSpans will modify the httptrace.ClientTrace to only collect data
// as Events and Attributes on a span found in the context. By default // as Events and Attributes on a span found in the context. By default
// sub-spans will be generated. // sub-spans will be generated.
//
// This option is recommended for services that make a large number of
// outbound HTTP requests per incoming request (e.g., API gateways,
// fan-out proxies, or GraphQL servers). Each outbound request creates
// up to 7 sub-spans (http.getconn, http.dns, http.connect, http.tls,
// http.headers, http.send, http.receive) as separate heap-allocated
// trace.Span objects. In high fan-out services this multiplied span
// volume can overwhelm the span processor queue and increase GC
// pressure, resulting in elevated and continuously growing memory usage.
// Using WithoutSubSpans replaces those spans with lightweight events on
// the parent span, preserving the diagnostic information at a fraction
// of the cost.
func WithoutSubSpans() ClientTraceOption { func WithoutSubSpans() ClientTraceOption {
return clientTraceOptionFunc(func(ct *clientTracer) { return clientTraceOptionFunc(func(ct *clientTracer) {
ct.useSpans = false ct.useSpans = false
@@ -189,10 +201,11 @@ func NewClientTrace(ctx context.Context, opts ...ClientTraceOption) *httptrace.C
} }
func (ct *clientTracer) start(hook, spanName string, attrs ...attribute.KeyValue) { func (ct *clientTracer) start(hook, spanName string, attrs ...attribute.KeyValue) {
if ct.root == nil {
ct.root = trace.SpanFromContext(ct.Context)
}
if !ct.useSpans { if !ct.useSpans {
if ct.root == nil {
ct.root = trace.SpanFromContext(ct.Context)
}
ct.root.AddEvent(hook+".start", trace.WithAttributes(attrs...)) ct.root.AddEvent(hook+".start", trace.WithAttributes(attrs...))
return return
} }
@@ -201,11 +214,7 @@ func (ct *clientTracer) start(hook, spanName string, attrs ...attribute.KeyValue
defer ct.mtx.Unlock() defer ct.mtx.Unlock()
if hookCtx, found := ct.activeHooks[hook]; !found { if hookCtx, found := ct.activeHooks[hook]; !found {
var sp trace.Span ct.activeHooks[hook], _ = ct.tr.Start(ct.getParentContext(hook), spanName, trace.WithAttributes(attrs...), trace.WithSpanKind(trace.SpanKindClient))
ct.activeHooks[hook], sp = ct.tr.Start(ct.getParentContext(hook), spanName, trace.WithAttributes(attrs...), trace.WithSpanKind(trace.SpanKindClient))
if ct.root == nil {
ct.root = sp
}
} else { } else {
// end was called before start finished, add the start attributes and end the span here // end was called before start finished, add the start attributes and end the span here
span := trace.SpanFromContext(hookCtx) span := trace.SpanFromContext(hookCtx)
@@ -306,14 +315,16 @@ func (ct *clientTracer) dnsDone(info httptrace.DNSDoneInfo) {
} }
func (ct *clientTracer) connectStart(network, addr string) { func (ct *clientTracer) connectStart(network, addr string) {
ct.start("http.connect."+addr, "http.connect", ct.start(
"http.connect."+addr, "http.connect",
HTTPRemoteAddr.String(addr), HTTPRemoteAddr.String(addr),
HTTPConnectionStartNetwork.String(network), HTTPConnectionStartNetwork.String(network),
) )
} }
func (ct *clientTracer) connectDone(network, addr string, err error) { func (ct *clientTracer) connectDone(network, addr string, err error) {
ct.end("http.connect."+addr, err, ct.end(
"http.connect."+addr, err,
HTTPConnectionDoneAddr.String(addr), HTTPConnectionDoneAddr.String(addr),
HTTPConnectionDoneNetwork.String(network), HTTPConnectionDoneNetwork.String(network),
) )
@@ -12,6 +12,7 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"reflect"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
@@ -19,8 +20,8 @@ import (
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/semconv/v1.40.0" "go.opentelemetry.io/otel/semconv/v1.41.0"
"go.opentelemetry.io/otel/semconv/v1.40.0/httpconv" "go.opentelemetry.io/otel/semconv/v1.41.0/httpconv"
) )
type HTTPClient struct { type HTTPClient struct {
@@ -166,7 +167,7 @@ func (n HTTPClient) ResponseTraceAttrs(resp *http.Response) []attribute.KeyValue
func (n HTTPClient) method(method string) (attribute.KeyValue, attribute.KeyValue) { func (n HTTPClient) method(method string) (attribute.KeyValue, attribute.KeyValue) {
if method == "" { if method == "" {
return semconv.HTTPRequestMethodGet, attribute.KeyValue{} return semconv.HTTPRequestMethodOther, attribute.KeyValue{}
} }
if attr, ok := methodLookup[method]; ok { if attr, ok := methodLookup[method]; ok {
return attr, attribute.KeyValue{} return attr, attribute.KeyValue{}
@@ -176,7 +177,7 @@ func (n HTTPClient) method(method string) (attribute.KeyValue, attribute.KeyValu
if attr, ok := methodLookup[strings.ToUpper(method)]; ok { if attr, ok := methodLookup[strings.ToUpper(method)]; ok {
return attr, orig return attr, orig
} }
return semconv.HTTPRequestMethodGet, orig return semconv.HTTPRequestMethodOther, orig
} }
func (n HTTPClient) MetricAttributes(req *http.Request, statusCode int, additionalAttributes []attribute.KeyValue) []attribute.KeyValue { func (n HTTPClient) MetricAttributes(req *http.Request, statusCode int, additionalAttributes []attribute.KeyValue) []attribute.KeyValue {
@@ -249,6 +250,9 @@ func (o MetricOpts) AddOptions() metric.AddOption {
func (n HTTPClient) MetricOptions(ma MetricAttributes) MetricOpts { func (n HTTPClient) MetricOptions(ma MetricAttributes) MetricOpts {
attributes := n.MetricAttributes(ma.Req, ma.StatusCode, ma.AdditionalAttributes) attributes := n.MetricAttributes(ma.Req, ma.StatusCode, ma.AdditionalAttributes)
if ma.StatusCode == 0 && ma.Err != nil {
attributes = append(attributes, n.ErrorType(ma.Err))
}
set := metric.WithAttributeSet(attribute.NewSet(attributes...)) set := metric.WithAttributeSet(attribute.NewSet(attributes...))
return MetricOpts{ return MetricOpts{
@@ -257,6 +261,39 @@ func (n HTTPClient) MetricOptions(ma MetricAttributes) MetricOpts {
} }
} }
// ErrorType returns an error.type attribute for the given error. The otelhttp
// Transport calls the underlying RoundTripper directly, so transport failures
// arrive as *net.OpError (connection refused, timeout, etc.) rather than
// *url.Error (which http.Client.Do adds above the Transport layer). This
// function intentionally does not unwrap further: reporting a single concrete
// type per failure keeps attribute cardinality bounded, as required by the
// OTel spec (error.type SHOULD have low cardinality). Callers that need
// finer-grained error distinctions should inspect the error themselves.
func (n HTTPClient) ErrorType(err error) attribute.KeyValue {
t := reflect.TypeOf(err)
if t == nil {
return semconv.ErrorTypeOther
}
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
var value string
if t.PkgPath() == "" || t.Name() == "" {
// t.PkgPath() == "" covers builtin and unnamed types.
// t.Name() == "" covers anonymous struct types that implement error,
// which are uncommon but possible. Fall back to t.String() for both.
value = t.String()
} else {
value = fmt.Sprintf("%s.%s", t.PkgPath(), t.Name())
}
if value == "" {
return semconv.ErrorTypeOther
}
return semconv.ErrorTypeKey.String(value)
}
func (n HTTPClient) RecordMetrics(ctx context.Context, md MetricData, opts MetricOpts) { func (n HTTPClient) RecordMetrics(ctx context.Context, md MetricData, opts MetricOpts) {
recordOpts := metricRecordOptionPool.Get().(*[]metric.RecordOption) recordOpts := metricRecordOptionPool.Get().(*[]metric.RecordOption)
defer func() { defer func() {

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