diff --git a/go.mod b/go.mod index ca6d53a4..03142793 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module code.cloudfoundry.org/cnbapplifecycle -go 1.26.1 +go 1.26.2 tool github.com/golangci/golangci-lint/v2/cmd/golangci-lint @@ -8,7 +8,7 @@ require ( code.cloudfoundry.org/credhub-cli v0.0.0-20260316130226-db948d6c5f1c github.com/BurntSushi/toml v1.6.0 github.com/apex/log v1.9.0 - github.com/buildpacks/lifecycle v0.21.7 + github.com/buildpacks/lifecycle v0.21.8 github.com/buildpacks/pack v0.40.3 github.com/cespare/xxhash/v2 v2.3.0 github.com/docker/docker v28.5.2+incompatible @@ -104,7 +104,7 @@ require ( github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect - github.com/containerd/platforms v1.0.0-rc.2 // indirect + github.com/containerd/platforms v1.0.0-rc.4 // indirect github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect @@ -143,7 +143,7 @@ require ( github.com/go-toolsmith/astp v1.1.0 // indirect github.com/go-toolsmith/strparse v1.1.0 // indirect github.com/go-toolsmith/typep v1.1.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/godoc-lint/godoc-lint v0.10.2 // indirect @@ -210,7 +210,7 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/ioprogress v0.0.0-20180201004757-6a23b12fa88e // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/moby/buildkit v0.28.1 // indirect + github.com/moby/buildkit v0.29.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.2.0 // indirect github.com/moby/moby/api v1.54.2 // indirect diff --git a/go.sum b/go.sum index aac13931..6ac0333a 100644 --- a/go.sum +++ b/go.sum @@ -163,8 +163,8 @@ github.com/breml/errchkjson v0.4.1 h1:keFSS8D7A2T0haP9kzZTi7o26r7kE3vymjZNeNDRDw github.com/breml/errchkjson v0.4.1/go.mod h1:a23OvR6Qvcl7DG/Z4o0el6BRAjKnaReoPQFciAl9U3s= github.com/buildpacks/imgutil v0.0.0-20260216175732-65b01efeb22e h1:is9Ms8aIyO6TMRQmWjzbhaAWmG6Qivi/M+wCmTEEHCc= github.com/buildpacks/imgutil v0.0.0-20260216175732-65b01efeb22e/go.mod h1:EBy9ZOatKkTBAWOgN2Y0aNV4P40dOdGyzzB9q2a5HBE= -github.com/buildpacks/lifecycle v0.21.7 h1:Du22qTALXsNgVrdH8wHNah3qtYp51NXpz+8XYdlvkls= -github.com/buildpacks/lifecycle v0.21.7/go.mod h1:k5p+XleTOlMA0RQKK30w+a0Nh7JVXn4XEUCEcszBTG0= +github.com/buildpacks/lifecycle v0.21.8 h1:dH9QUJdIRpZHBCcQ3CxQeeIdjSDWsdFHYb1aYbYBeJo= +github.com/buildpacks/lifecycle v0.21.8/go.mod h1:p4UUOU4p/XaUm5u2X0eZO6Lt9b8LKqg4df62PJRUNLU= github.com/buildpacks/pack v0.40.3 h1:JCBsul/yYfHEYv8whNkKUztOUthP8PAS5TW/x2WR8vQ= github.com/buildpacks/pack v0.40.3/go.mod h1:Cw9SRAL7Tzo0tGGyXlokO4ciqu1FRs9jcvZqtXcUCeo= github.com/butuzov/ireturn v0.4.0 h1:+s76bF/PfeKEdbG8b54aCocxXmi0wvYdOVsWxVO7n8E= @@ -213,8 +213,8 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4= -github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= +github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4= +github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= @@ -340,8 +340,8 @@ github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQi github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY= github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= @@ -422,8 +422,8 @@ github.com/gostaticanalysis/nilerr v0.1.2/go.mod h1:A19UHhoY3y8ahoL7YKz6sdjDtduw github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8= github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo= github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= @@ -548,8 +548,8 @@ github.com/mitchellh/ioprogress v0.0.0-20180201004757-6a23b12fa88e h1:Qa6dnn8Dla github.com/mitchellh/ioprogress v0.0.0-20180201004757-6a23b12fa88e/go.mod h1:waEya8ee1Ro/lgxpVhkJI4BVASzkm3UZqkx/cFJiYHM= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/moby/buildkit v0.28.1 h1:Tq6H6gOMU2JyEQ5rA0pa7Ey3VGNR3qpw90liSIpMQoo= -github.com/moby/buildkit v0.28.1/go.mod h1:xO6wb9VBXszkIBxaGTLXc1rQORVQFIJRt3GSX7KzCFc= +github.com/moby/buildkit v0.29.0 h1:wxLEFbCOJntEDjSNNN2YWd8zxltZxT5muDQ0LzpbtpU= +github.com/moby/buildkit v0.29.0/go.mod h1:Dmv2FeDe34t75QuzeU87rBoZpAAkcpT5zeu4hXzmASc= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= @@ -841,8 +841,8 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:Oyrsyzu go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 h1:inYW9ZhgqiDqh6BioM7DVHHzEGVq76Db5897WLGZ5Go= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0/go.mod h1:Izur+Wt8gClgMJqO/cZ8wdeeMryJ/xxiOVgFSSfpDTY= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= @@ -853,8 +853,8 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfC go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= -go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= -go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/vendor/github.com/containerd/platforms/.golangci.yml b/vendor/github.com/containerd/platforms/.golangci.yml index d574fe11..9320503c 100644 --- a/vendor/github.com/containerd/platforms/.golangci.yml +++ b/vendor/github.com/containerd/platforms/.golangci.yml @@ -1,32 +1,25 @@ +version: "2" linters: enable: - copyloopvar - - gofmt - - goimports + - dupword - gosec - - ineffassign - misspell - nolintlint - revive - - staticcheck - - tenv # Detects using os.Setenv instead of t.Setenv since Go 1.17 - unconvert - - unused - - govet - - dupword # Checks for duplicate words in the source code disable: - errcheck - -run: - timeout: 5m - -issues: - exclude-dirs: - - api - - cluster - - design - - docs - - docs/man - - releases - - reports - - test # e2e scripts + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax diff --git a/vendor/github.com/containerd/platforms/compare.go b/vendor/github.com/containerd/platforms/compare.go index 24403f3b..ea5e7aa1 100644 --- a/vendor/github.com/containerd/platforms/compare.go +++ b/vendor/github.com/containerd/platforms/compare.go @@ -152,6 +152,88 @@ func Only(platform specs.Platform) MatchComparer { return Ordered(platformVector(Normalize(platform))...) } +// OnlyOS returns a match comparer that matches only platforms with the same +// OS, OS version, and OS features, regardless of architecture. When comparing, +// it always ranks the best architecture match highest using the default +// platform resolution logic. +func OnlyOS(platform specs.Platform) MatchComparer { + normalized := Normalize(platform) + return onlyOSComparer{ + platform: normalized, + osvM: newOSVersionMatcher(normalized), + archOrder: orderedPlatformComparer{ + matchers: []Matcher{NewMatcher(normalized)}, + }, + } +} + +func newOSVersionMatcher(platform specs.Platform) osVerMatcher { + if platform.OS == "windows" { + return &windowsVersionMatcher{ + windowsOSVersion: getWindowsOSVersion(platform.OSVersion), + } + } + return nil +} + +type onlyOSComparer struct { + platform specs.Platform + osvM osVerMatcher + archOrder orderedPlatformComparer +} + +func (c onlyOSComparer) matchOS(platform specs.Platform) bool { + normalized := Normalize(platform) + if c.platform.OS != normalized.OS { + return false + } + if c.osvM != nil { + if !c.osvM.Match(platform.OSVersion) { + return false + } + } + if len(normalized.OSFeatures) > 0 { + if len(c.platform.OSFeatures) < len(normalized.OSFeatures) { + return false + } + j := 0 + for _, feature := range normalized.OSFeatures { + found := false + for ; j < len(c.platform.OSFeatures); j++ { + if feature == c.platform.OSFeatures[j] { + found = true + j++ + break + } + if feature < c.platform.OSFeatures[j] { + return false + } + } + if !found { + return false + } + } + } + return true +} + +func (c onlyOSComparer) Match(platform specs.Platform) bool { + return c.matchOS(platform) +} + +func (c onlyOSComparer) Less(p1, p2 specs.Platform) bool { + p1m := c.matchOS(p1) + p2m := c.matchOS(p2) + if p1m && !p2m { + return true + } + if !p1m { + return false + } + // Both match — rank by architecture preference + return c.archOrder.Less(p1, p2) +} + // OnlyStrict returns a match comparer for a single platform. // // Unlike Only, OnlyStrict does not match sub platforms. @@ -213,9 +295,20 @@ func (c orderedPlatformComparer) Less(p1 specs.Platform, p2 specs.Platform) bool return true } if p1m || p2m { + if p1m && p2m { + // Prefer one with most matching features + if len(p1.OSFeatures) != len(p2.OSFeatures) { + return len(p1.OSFeatures) > len(p2.OSFeatures) + } + } return false } } + if len(p1.OSFeatures) > 0 || len(p2.OSFeatures) > 0 { + p1.OSFeatures = nil + p2.OSFeatures = nil + return c.Less(p1, p2) + } return false } @@ -242,9 +335,20 @@ func (c anyPlatformComparer) Less(p1, p2 specs.Platform) bool { p2m = true } if p1m && p2m { - return false + if len(p1.OSFeatures) != len(p2.OSFeatures) { + return len(p1.OSFeatures) > len(p2.OSFeatures) + } + break } } + + // If neither match and has features, strip features and compare + if !p1m && !p2m && (len(p1.OSFeatures) > 0 || len(p2.OSFeatures) > 0) { + p1.OSFeatures = nil + p2.OSFeatures = nil + return c.Less(p1, p2) + } + // If one matches, and the other does, sort match first return p1m && !p2m } diff --git a/vendor/github.com/containerd/platforms/cpuinfo_linux.go b/vendor/github.com/containerd/platforms/cpuinfo_linux.go index 98c7001f..06da8b96 100644 --- a/vendor/github.com/containerd/platforms/cpuinfo_linux.go +++ b/vendor/github.com/containerd/platforms/cpuinfo_linux.go @@ -45,7 +45,6 @@ func getMachineArch() (string, error) { // So we don't need to access the ARM registers to detect platform information // by ourselves. We can just parse these information from /proc/cpuinfo func getCPUInfo(pattern string) (info string, err error) { - cpuinfo, err := os.Open("/proc/cpuinfo") if err != nil { return "", err @@ -75,7 +74,6 @@ func getCPUInfo(pattern string) (info string, err error) { // getCPUVariantFromArch get CPU variant from arch through a system call func getCPUVariantFromArch(arch string) (string, error) { - var variant string arch = strings.ToLower(arch) diff --git a/vendor/github.com/containerd/platforms/cpuinfo_other.go b/vendor/github.com/containerd/platforms/cpuinfo_other.go index 97a1fe8a..b8c7a4b2 100644 --- a/vendor/github.com/containerd/platforms/cpuinfo_other.go +++ b/vendor/github.com/containerd/platforms/cpuinfo_other.go @@ -24,10 +24,10 @@ import ( ) func getCPUVariant() (string, error) { - var variant string - if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { + switch runtime.GOOS { + case "windows", "darwin": // Windows/Darwin only supports v7 for ARM32 and v8 for ARM64 and so we can use // runtime.GOARCH to determine the variants switch runtime.GOARCH { @@ -38,7 +38,7 @@ func getCPUVariant() (string, error) { default: variant = "unknown" } - } else if runtime.GOOS == "freebsd" { + case "freebsd": // FreeBSD supports ARMv6 and ARMv7 as well as ARMv4 and ARMv5 (though deprecated) // detecting those variants is currently unimplemented switch runtime.GOARCH { @@ -47,7 +47,7 @@ func getCPUVariant() (string, error) { default: variant = "unknown" } - } else { + default: return "", fmt.Errorf("getCPUVariant for OS %s: %v", runtime.GOOS, errNotImplemented) } diff --git a/vendor/github.com/containerd/platforms/platform_windows_compat.go b/vendor/github.com/containerd/platforms/platform_windows_compat.go index f31ebe0c..ef21a290 100644 --- a/vendor/github.com/containerd/platforms/platform_windows_compat.go +++ b/vendor/github.com/containerd/platforms/platform_windows_compat.go @@ -17,6 +17,7 @@ package platforms import ( + "slices" "strconv" "strings" @@ -162,3 +163,14 @@ func (c *windowsMatchComparer) Less(p1, p2 specs.Platform) bool { } return m1 && !m2 } + +type windowsStripFeaturesMatcher struct { + Matcher +} + +func (m windowsStripFeaturesMatcher) Match(p specs.Platform) bool { + if i := slices.Index(p.OSFeatures, "win32k"); i >= 0 { + p.OSFeatures = slices.Delete(slices.Clone(p.OSFeatures), i, i+1) + } + return m.Matcher.Match(p) +} diff --git a/vendor/github.com/containerd/platforms/platforms.go b/vendor/github.com/containerd/platforms/platforms.go index 14d65abd..81d7ee38 100644 --- a/vendor/github.com/containerd/platforms/platforms.go +++ b/vendor/github.com/containerd/platforms/platforms.go @@ -111,9 +111,11 @@ package platforms import ( "fmt" + "net/url" "path" "regexp" "runtime" + "slices" "strconv" "strings" @@ -121,12 +123,10 @@ import ( ) var ( - specifierRe = regexp.MustCompile(`^[A-Za-z0-9_.-]+$`) - osAndVersionRe = regexp.MustCompile(`^([A-Za-z0-9_-]+)(?:\(([A-Za-z0-9_.-]*)\))?$`) + specifierRe = regexp.MustCompile(`^[A-Za-z0-9_.-]+$`) + osRe = regexp.MustCompile(`^([A-Za-z0-9_-]+)(?:\(([A-Za-z0-9_.%-]*)((?:\+[A-Za-z0-9_.%-]+)*)\))?$`) ) -const osAndVersionFormat = "%s(%s)" - // Platform is a type alias for convenience, so there is no need to import image-spec package everywhere. type Platform = specs.Platform @@ -143,6 +143,10 @@ type Matcher interface { // functionality. // // Applications should opt to use `Match` over directly parsing specifiers. +// +// For OSFeatures, this matcher will match if the platform to match has +// OSFeatures which are a subset of the OSFeatures of the platform +// provided to NewMatcher. func NewMatcher(platform specs.Platform) Matcher { m := &matcher{ Platform: Normalize(platform), @@ -152,6 +156,11 @@ func NewMatcher(platform specs.Platform) Matcher { m.osvM = &windowsVersionMatcher{ windowsOSVersion: getWindowsOSVersion(platform.OSVersion), } + + // In prior versions, the win32k os feature was not considered for matching, + // strip out the win32k feature for comparison + var stripped Matcher = windowsStripFeaturesMatcher{m} + // In prior versions, on windows, the returned matcher implements a // MatchComprarer interface. // This preserves that behavior for backwards compatibility. @@ -161,8 +170,9 @@ func NewMatcher(platform specs.Platform) Matcher { // It was likely intended to be used in `Ordered` but it is not since // `Less` that is implemented here ends up getting masked due to wrapping. if runtime.GOOS == "windows" { - return &windowsMatchComparer{m} + return &windowsMatchComparer{stripped} } + return stripped } return m } @@ -178,10 +188,39 @@ type matcher struct { func (m *matcher) Match(platform specs.Platform) bool { normalized := Normalize(platform) - return m.OS == normalized.OS && + if m.OS == normalized.OS && m.Architecture == normalized.Architecture && m.Variant == normalized.Variant && - m.matchOSVersion(platform) + m.matchOSVersion(platform) { + if len(normalized.OSFeatures) == 0 { + return true + } + if len(m.OSFeatures) >= len(normalized.OSFeatures) { + // Ensure that normalized.OSFeatures is a subset of + // m.OSFeatures + j := 0 + for _, feature := range normalized.OSFeatures { + found := false + for ; j < len(m.OSFeatures); j++ { + if feature == m.OSFeatures[j] { + found = true + j++ + break + } + // Since both lists are ordered, if the feature is less + // than what is seen, it is not in the list + if feature < m.OSFeatures[j] { + return false + } + } + if !found { + return false + } + } + return true + } + } + return false } func (m *matcher) matchOSVersion(platform specs.Platform) bool { @@ -210,11 +249,14 @@ func ParseAll(specifiers []string) ([]specs.Platform, error) { // Parse parses the platform specifier syntax into a platform declaration. // -// Platform specifiers are in the format `[()]||[()]/[/]`. +// Platform specifiers are in the format `[()]||[()]/[/]`. // The minimum required information for a platform specifier is the operating -// system or architecture. The OSVersion can be part of the OS like `windows(10.0.17763)` -// When an OSVersion is specified, then specs.Platform.OSVersion is populated with that value, -// and an empty string otherwise. +// system or architecture. The "os options" may be OSVersion which can be part of the OS +// like `windows(10.0.17763)`. When an OSVersion is specified, then specs.Platform.OSVersion is +// populated with that value, and an empty string otherwise. The "os options" may also include an +// array of OSFeatures, each feature prefixed with '+', without any other separator, and provided +// after the OSVersion when the OSVersion is specified. An "os options" with version and features +// is like `windows(10.0.17763+win32k)`. // If there is only a single string (no slashes), the // value will be matched against the known set of operating systems, then fall // back to the known set of architectures. The missing component will be @@ -231,14 +273,24 @@ func Parse(specifier string) (specs.Platform, error) { var p specs.Platform for i, part := range parts { if i == 0 { - // First element is [()] - osVer := osAndVersionRe.FindStringSubmatch(part) - if osVer == nil { - return specs.Platform{}, fmt.Errorf("%q is an invalid OS component of %q: OSAndVersion specifier component must match %q: %w", part, specifier, osAndVersionRe.String(), errInvalidArgument) + // First element is [([+]*)] + osOptions := osRe.FindStringSubmatch(part) + if osOptions == nil { + return specs.Platform{}, fmt.Errorf("%q is an invalid OS component of %q: OSAndVersion specifier component must match %q: %w", part, specifier, osRe.String(), errInvalidArgument) } - p.OS = normalizeOS(osVer[1]) - p.OSVersion = osVer[2] + p.OS = normalizeOS(osOptions[1]) + osVersion, err := decodeOSOption(osOptions[2]) + if err != nil { + return specs.Platform{}, fmt.Errorf("%q has an invalid OS version %q: %w", specifier, osOptions[2], err) + } + p.OSVersion = osVersion + if osOptions[3] != "" { + p.OSFeatures, err = parseOSFeatures(osOptions[3][1:]) + if err != nil { + return specs.Platform{}, fmt.Errorf("%q has invalid OS features: %w", specifier, err) + } + } } else { if !specifierRe.MatchString(part) { return specs.Platform{}, fmt.Errorf("%q is an invalid component of %q: platform specifier component must match %q: %w", part, specifier, specifierRe.String(), errInvalidArgument) @@ -296,6 +348,30 @@ func Parse(specifier string) (specs.Platform, error) { return specs.Platform{}, fmt.Errorf("%q: cannot parse platform specifier: %w", specifier, errInvalidArgument) } +func parseOSFeatures(s string) ([]string, error) { + if s == "" { + return nil, nil + } + + var features []string + for raw := range strings.SplitSeq(s, "+") { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil, fmt.Errorf("empty os feature: %w", errInvalidArgument) + } + feature, err := decodeOSOption(raw) + if err != nil { + return nil, fmt.Errorf("invalid os feature %q: %w", raw, err) + } + if feature == "" { + continue + } + features = append(features, feature) + } + + return features, nil +} + // MustParse is like Parses but panics if the specifier cannot be parsed. // Simplifies initialization of global variables. func MustParse(specifier string) specs.Platform { @@ -321,12 +397,77 @@ func FormatAll(platform specs.Platform) string { if platform.OS == "" { return "unknown" } + if platform.OSVersion == "" && len(platform.OSFeatures) == 0 { + return path.Join(platform.OS, platform.Architecture, platform.Variant) + } - if platform.OSVersion != "" { - OSAndVersion := fmt.Sprintf(osAndVersionFormat, platform.OS, platform.OSVersion) - return path.Join(OSAndVersion, 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(platform.OS, platform.Architecture, platform.Variant) + + return path.Join(b.String(), platform.Architecture, platform.Variant) +} + +func formatOSFeatures(features []string) string { + if len(features) == 0 { + return "" + } + + if !slices.IsSorted(features) { + features = slices.Clone(features) + slices.Sort(features) + } + var b strings.Builder + var wrote bool + var prev string + for _, f := range features { + if f == "" || f == prev { + // skip empty and duplicate values + continue + } + prev = f + if wrote { + b.WriteByte('+') + } + b.WriteString(encodeOSOption(f)) + wrote = true + } + return b.String() +} + +// osOptionReplacer encodes characters in OS option values (version and +// features) that are ambiguous with the format syntax. The percent sign +// must be replaced first to avoid double-encoding. +var osOptionReplacer = strings.NewReplacer( + "%", "%25", + "+", "%2B", + "(", "%28", + ")", "%29", + "/", "%2F", +) + +func encodeOSOption(v string) string { + return osOptionReplacer.Replace(v) +} + +func decodeOSOption(v string) (string, error) { + if strings.Contains(v, "%") { + return url.PathUnescape(v) + } + return v, nil } // Normalize validates and translate the platform to the canonical value. @@ -336,6 +477,11 @@ func FormatAll(platform specs.Platform) string { func Normalize(platform specs.Platform) specs.Platform { platform.OS = normalizeOS(platform.OS) platform.Architecture, platform.Variant = normalizeArch(platform.Architecture, platform.Variant) + if len(platform.OSFeatures) > 0 { + platform.OSFeatures = slices.Clone(platform.OSFeatures) + slices.Sort(platform.OSFeatures) + platform.OSFeatures = slices.Compact(platform.OSFeatures) + } return platform } diff --git a/vendor/github.com/go-viper/mapstructure/v2/.editorconfig b/vendor/github.com/go-viper/mapstructure/v2/.editorconfig index faef0c91..c37602a0 100644 --- a/vendor/github.com/go-viper/mapstructure/v2/.editorconfig +++ b/vendor/github.com/go-viper/mapstructure/v2/.editorconfig @@ -19,3 +19,6 @@ indent_size = 2 [.golangci.yaml] indent_size = 2 + +[devenv.yaml] +indent_size = 2 diff --git a/vendor/github.com/go-viper/mapstructure/v2/.envrc b/vendor/github.com/go-viper/mapstructure/v2/.envrc index 2e0f9f5f..e2be8891 100644 --- a/vendor/github.com/go-viper/mapstructure/v2/.envrc +++ b/vendor/github.com/go-viper/mapstructure/v2/.envrc @@ -1,4 +1,7 @@ -if ! has nix_direnv_version || ! nix_direnv_version 3.0.4; then - source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.4/direnvrc" "sha256-DzlYZ33mWF/Gs8DDeyjr8mnVmQGx7ASYqA5WlxwvBG4=" -fi -use flake . --impure +#!/usr/bin/env bash + +export DIRENV_WARN_TIMEOUT=20s + +eval "$(devenv direnvrc)" + +use devenv diff --git a/vendor/github.com/go-viper/mapstructure/v2/.gitignore b/vendor/github.com/go-viper/mapstructure/v2/.gitignore index 470e7ca2..71caea19 100644 --- a/vendor/github.com/go-viper/mapstructure/v2/.gitignore +++ b/vendor/github.com/go-viper/mapstructure/v2/.gitignore @@ -1,6 +1,10 @@ -/.devenv/ -/.direnv/ -/.pre-commit-config.yaml /bin/ /build/ /var/ + +# Devenv +.devenv* +devenv.local.nix +devenv.local.yaml +.direnv +.pre-commit-config.yaml diff --git a/vendor/github.com/go-viper/mapstructure/v2/devenv.lock b/vendor/github.com/go-viper/mapstructure/v2/devenv.lock new file mode 100644 index 00000000..72c2c9b4 --- /dev/null +++ b/vendor/github.com/go-viper/mapstructure/v2/devenv.lock @@ -0,0 +1,103 @@ +{ + "nodes": { + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1765288076, + "owner": "cachix", + "repo": "devenv", + "rev": "93c055af1e8fcac49251f1b2e1c57f78620ad351", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1765121682, + "owner": "edolstra", + "repo": "flake-compat", + "rev": "65f23138d8d09a92e30f1e5c87611b23ef451bf3", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1765016596, + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "548fc44fca28a5e81c5d6b846e555e6b9c2a5a3c", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1762808025, + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1764580874, + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "dcf61356c3ab25f1362b4a4428a6d871e84f1d1d", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "git-hooks": "git-hooks", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": [ + "git-hooks" + ] + } + } + }, + "root": "root", + "version": 7 +} diff --git a/vendor/github.com/go-viper/mapstructure/v2/devenv.nix b/vendor/github.com/go-viper/mapstructure/v2/devenv.nix new file mode 100644 index 00000000..b31ab7a1 --- /dev/null +++ b/vendor/github.com/go-viper/mapstructure/v2/devenv.nix @@ -0,0 +1,14 @@ +{ + pkgs, + ... +}: + +{ + languages = { + go.enable = true; + }; + + packages = with pkgs; [ + golangci-lint + ]; +} diff --git a/vendor/github.com/go-viper/mapstructure/v2/devenv.yaml b/vendor/github.com/go-viper/mapstructure/v2/devenv.yaml new file mode 100644 index 00000000..68616a49 --- /dev/null +++ b/vendor/github.com/go-viper/mapstructure/v2/devenv.yaml @@ -0,0 +1,4 @@ +# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json +inputs: + nixpkgs: + url: github:cachix/devenv-nixpkgs/rolling diff --git a/vendor/github.com/go-viper/mapstructure/v2/flake.lock b/vendor/github.com/go-viper/mapstructure/v2/flake.lock deleted file mode 100644 index 5e67bdd6..00000000 --- a/vendor/github.com/go-viper/mapstructure/v2/flake.lock +++ /dev/null @@ -1,294 +0,0 @@ -{ - "nodes": { - "cachix": { - "inputs": { - "devenv": [ - "devenv" - ], - "flake-compat": [ - "devenv" - ], - "git-hooks": [ - "devenv" - ], - "nixpkgs": "nixpkgs" - }, - "locked": { - "lastModified": 1742042642, - "narHash": "sha256-D0gP8srrX0qj+wNYNPdtVJsQuFzIng3q43thnHXQ/es=", - "owner": "cachix", - "repo": "cachix", - "rev": "a624d3eaf4b1d225f918de8543ed739f2f574203", - "type": "github" - }, - "original": { - "owner": "cachix", - "ref": "latest", - "repo": "cachix", - "type": "github" - } - }, - "devenv": { - "inputs": { - "cachix": "cachix", - "flake-compat": "flake-compat", - "git-hooks": "git-hooks", - "nix": "nix", - "nixpkgs": "nixpkgs_3" - }, - "locked": { - "lastModified": 1744876578, - "narHash": "sha256-8MTBj2REB8t29sIBLpxbR0+AEGJ7f+RkzZPAGsFd40c=", - "owner": "cachix", - "repo": "devenv", - "rev": "7ff7c351bba20d0615be25ecdcbcf79b57b85fe1", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "devenv", - "type": "github" - } - }, - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1733328505, - "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, - "flake-parts": { - "inputs": { - "nixpkgs-lib": [ - "devenv", - "nix", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1712014858, - "narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "9126214d0a59633752a136528f5f3b9aa8565b7d", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "flake-parts", - "type": "github" - } - }, - "flake-parts_2": { - "inputs": { - "nixpkgs-lib": "nixpkgs-lib" - }, - "locked": { - "lastModified": 1743550720, - "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "c621e8422220273271f52058f618c94e405bb0f5", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "flake-parts", - "type": "github" - } - }, - "git-hooks": { - "inputs": { - "flake-compat": [ - "devenv" - ], - "gitignore": "gitignore", - "nixpkgs": [ - "devenv", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1742649964, - "narHash": "sha256-DwOTp7nvfi8mRfuL1escHDXabVXFGT1VlPD1JHrtrco=", - "owner": "cachix", - "repo": "git-hooks.nix", - "rev": "dcf5072734cb576d2b0c59b2ac44f5050b5eac82", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "git-hooks.nix", - "type": "github" - } - }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "devenv", - "git-hooks", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1709087332, - "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", - "type": "github" - } - }, - "libgit2": { - "flake": false, - "locked": { - "lastModified": 1697646580, - "narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=", - "owner": "libgit2", - "repo": "libgit2", - "rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5", - "type": "github" - }, - "original": { - "owner": "libgit2", - "repo": "libgit2", - "type": "github" - } - }, - "nix": { - "inputs": { - "flake-compat": [ - "devenv" - ], - "flake-parts": "flake-parts", - "libgit2": "libgit2", - "nixpkgs": "nixpkgs_2", - "nixpkgs-23-11": [ - "devenv" - ], - "nixpkgs-regression": [ - "devenv" - ], - "pre-commit-hooks": [ - "devenv" - ] - }, - "locked": { - "lastModified": 1741798497, - "narHash": "sha256-E3j+3MoY8Y96mG1dUIiLFm2tZmNbRvSiyN7CrSKuAVg=", - "owner": "domenkozar", - "repo": "nix", - "rev": "f3f44b2baaf6c4c6e179de8cbb1cc6db031083cd", - "type": "github" - }, - "original": { - "owner": "domenkozar", - "ref": "devenv-2.24", - "repo": "nix", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1733212471, - "narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "55d15ad12a74eb7d4646254e13638ad0c4128776", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-lib": { - "locked": { - "lastModified": 1743296961, - "narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=", - "owner": "nix-community", - "repo": "nixpkgs.lib", - "rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nixpkgs.lib", - "type": "github" - } - }, - "nixpkgs_2": { - "locked": { - "lastModified": 1717432640, - "narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "88269ab3044128b7c2f4c7d68448b2fb50456870", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "release-24.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_3": { - "locked": { - "lastModified": 1733477122, - "narHash": "sha256-qamMCz5mNpQmgBwc8SB5tVMlD5sbwVIToVZtSxMph9s=", - "owner": "cachix", - "repo": "devenv-nixpkgs", - "rev": "7bd9e84d0452f6d2e63b6e6da29fe73fac951857", - "type": "github" - }, - "original": { - "owner": "cachix", - "ref": "rolling", - "repo": "devenv-nixpkgs", - "type": "github" - } - }, - "nixpkgs_4": { - "locked": { - "lastModified": 1744536153, - "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "devenv": "devenv", - "flake-parts": "flake-parts_2", - "nixpkgs": "nixpkgs_4" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/vendor/github.com/go-viper/mapstructure/v2/flake.nix b/vendor/github.com/go-viper/mapstructure/v2/flake.nix deleted file mode 100644 index 3b116f42..00000000 --- a/vendor/github.com/go-viper/mapstructure/v2/flake.nix +++ /dev/null @@ -1,46 +0,0 @@ -{ - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - flake-parts.url = "github:hercules-ci/flake-parts"; - devenv.url = "github:cachix/devenv"; - }; - - outputs = - inputs@{ flake-parts, ... }: - flake-parts.lib.mkFlake { inherit inputs; } { - imports = [ - inputs.devenv.flakeModule - ]; - - systems = [ - "x86_64-linux" - "x86_64-darwin" - "aarch64-darwin" - ]; - - perSystem = - { pkgs, ... }: - rec { - devenv.shells = { - default = { - languages = { - go.enable = true; - }; - - pre-commit.hooks = { - nixpkgs-fmt.enable = true; - }; - - packages = with pkgs; [ - golangci-lint - ]; - - # https://github.com/cachix/devenv/issues/528#issuecomment-1556108767 - containers = pkgs.lib.mkForce { }; - }; - - ci = devenv.shells.default; - }; - }; - }; -} diff --git a/vendor/github.com/go-viper/mapstructure/v2/mapstructure.go b/vendor/github.com/go-viper/mapstructure/v2/mapstructure.go index 7c35bce0..9087fd96 100644 --- a/vendor/github.com/go-viper/mapstructure/v2/mapstructure.go +++ b/vendor/github.com/go-viper/mapstructure/v2/mapstructure.go @@ -173,6 +173,25 @@ // Public: "I made it through!" // } // +// # Custom Decoding with Unmarshaler +// +// Types can implement the Unmarshaler interface to control their own decoding. The interface +// behaves similarly to how UnmarshalJSON does in the standard library. It can be used as an +// alternative or companion to a DecodeHook. +// +// type TrimmedString string +// +// func (t *TrimmedString) UnmarshalMapstructure(input any) error { +// str, ok := input.(string) +// if !ok { +// return fmt.Errorf("expected string, got %T", input) +// } +// *t = TrimmedString(strings.TrimSpace(str)) +// return nil +// } +// +// See the Unmarshaler interface documentation for more details. +// // # Other Configuration // // mapstructure is highly configurable. See the DecoderConfig struct @@ -218,6 +237,17 @@ type DecodeHookFuncKind func(reflect.Kind, reflect.Kind, any) (any, error) // values. type DecodeHookFuncValue func(from reflect.Value, to reflect.Value) (any, error) +// Unmarshaler is the interface implemented by types that can unmarshal +// themselves. UnmarshalMapstructure receives the input data (potentially +// transformed by DecodeHook) and should populate the receiver with the +// decoded values. +// +// The Unmarshaler interface takes precedence over the default decoding +// logic for any type (structs, slices, maps, primitives, etc.). +type Unmarshaler interface { + UnmarshalMapstructure(any) error +} + // DecoderConfig is the configuration that is used to create a new decoder // and allows customization of various aspects of decoding. type DecoderConfig struct { @@ -281,6 +311,13 @@ type DecoderConfig struct { // } Squash bool + // Deep will map structures in slices instead of copying them + // + // type Parent struct { + // Children []Child `mapstructure:",deep"` + // } + Deep bool + // Metadata is the struct that will contain extra metadata about // the decoding. If this is nil, then no metadata will be tracked. Metadata *Metadata @@ -290,9 +327,15 @@ type DecoderConfig struct { Result any // The tag name that mapstructure reads for field names. This - // defaults to "mapstructure" + // defaults to "mapstructure". Multiple tag names can be specified + // as a comma-separated list (e.g., "yaml,json"), and the first + // matching non-empty tag will be used. TagName string + // RootName specifies the name to use for the root element in error messages. For example: + // '' has unset fields: + RootName string + // The option of the value in the tag that indicates a field should // be squashed. This defaults to "squash". SquashTagOption string @@ -304,11 +347,34 @@ type DecoderConfig struct { // MatchName is the function used to match the map key to the struct // field name or tag. Defaults to `strings.EqualFold`. This can be used // to implement case-sensitive tag values, support snake casing, etc. + // + // MatchName is used as a fallback comparison when the direct key lookup fails. + // See also MapFieldName for transforming field names before lookup. MatchName func(mapKey, fieldName string) bool // DecodeNil, if set to true, will cause the DecodeHook (if present) to run // even if the input is nil. This can be used to provide default values. DecodeNil bool + + // MapFieldName is the function used to convert the struct field name to the map's key name. + // + // This is useful for automatically converting between naming conventions without + // explicitly tagging each field. For example, to convert Go's PascalCase field names + // to snake_case map keys: + // + // MapFieldName: func(s string) string { + // return strcase.ToSnake(s) + // } + // + // When decoding from a map to a struct, the transformed field name is used for + // the initial lookup. If not found, MatchName is used as a fallback comparison. + // Explicit struct tags always take precedence over MapFieldName. + MapFieldName func(string) string + + // DisableUnmarshaler, if set to true, disables the use of the Unmarshaler + // interface. Types implementing Unmarshaler will be decoded using the + // standard struct decoding logic instead. + DisableUnmarshaler bool } // A Decoder takes a raw interface value and turns it into structured @@ -445,6 +511,12 @@ func NewDecoder(config *DecoderConfig) (*Decoder, error) { config.MatchName = strings.EqualFold } + if config.MapFieldName == nil { + config.MapFieldName = func(s string) string { + return s + } + } + result := &Decoder{ config: config, } @@ -458,7 +530,7 @@ func NewDecoder(config *DecoderConfig) (*Decoder, error) { // Decode decodes the given raw interface to the target pointer specified // by the configuration. func (d *Decoder) Decode(input any) error { - err := d.decode("", input, reflect.ValueOf(d.config.Result).Elem()) + err := d.decode(d.config.RootName, input, reflect.ValueOf(d.config.Result).Elem()) // Retain some of the original behavior when multiple errors ocurr var joinedErr interface{ Unwrap() []error } @@ -540,36 +612,50 @@ func (d *Decoder) decode(name string, input any, outVal reflect.Value) error { var err error addMetaKey := true - switch outputKind { - case reflect.Bool: - err = d.decodeBool(name, input, outVal) - case reflect.Interface: - err = d.decodeBasic(name, input, outVal) - case reflect.String: - err = d.decodeString(name, input, outVal) - case reflect.Int: - err = d.decodeInt(name, input, outVal) - case reflect.Uint: - err = d.decodeUint(name, input, outVal) - case reflect.Float32: - err = d.decodeFloat(name, input, outVal) - case reflect.Complex64: - err = d.decodeComplex(name, input, outVal) - case reflect.Struct: - err = d.decodeStruct(name, input, outVal) - case reflect.Map: - err = d.decodeMap(name, input, outVal) - case reflect.Ptr: - addMetaKey, err = d.decodePtr(name, input, outVal) - case reflect.Slice: - err = d.decodeSlice(name, input, outVal) - case reflect.Array: - err = d.decodeArray(name, input, outVal) - case reflect.Func: - err = d.decodeFunc(name, input, outVal) - default: - // If we reached this point then we weren't able to decode it - return newDecodeError(name, fmt.Errorf("unsupported type: %s", outputKind)) + + // Check if the target implements Unmarshaler and use it if not disabled + unmarshaled := false + if !d.config.DisableUnmarshaler { + if unmarshaler, ok := getUnmarshaler(outVal); ok { + if err = unmarshaler.UnmarshalMapstructure(input); err != nil { + err = newDecodeError(name, err) + } + unmarshaled = true + } + } + + if !unmarshaled { + switch outputKind { + case reflect.Bool: + err = d.decodeBool(name, input, outVal) + case reflect.Interface: + err = d.decodeBasic(name, input, outVal) + case reflect.String: + err = d.decodeString(name, input, outVal) + case reflect.Int: + err = d.decodeInt(name, input, outVal) + case reflect.Uint: + err = d.decodeUint(name, input, outVal) + case reflect.Float32: + err = d.decodeFloat(name, input, outVal) + case reflect.Complex64: + err = d.decodeComplex(name, input, outVal) + case reflect.Struct: + err = d.decodeStruct(name, input, outVal) + case reflect.Map: + err = d.decodeMap(name, input, outVal) + case reflect.Ptr: + addMetaKey, err = d.decodePtr(name, input, outVal) + case reflect.Slice: + err = d.decodeSlice(name, input, outVal) + case reflect.Array: + err = d.decodeArray(name, input, outVal) + case reflect.Func: + err = d.decodeFunc(name, input, outVal) + default: + // If we reached this point then we weren't able to decode it + return newDecodeError(name, fmt.Errorf("unsupported type: %s", outputKind)) + } } // If we reached here, then we successfully decoded SOMETHING, so @@ -668,7 +754,7 @@ func (d *Decoder) decodeString(name string, data any, val reflect.Value) error { case reflect.Uint8: var uints []uint8 if dataKind == reflect.Array { - uints = make([]uint8, dataVal.Len(), dataVal.Len()) + uints = make([]uint8, dataVal.Len()) for i := range uints { uints[i] = dataVal.Index(i).Interface().(uint8) } @@ -1060,8 +1146,8 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re ) } - tagValue := f.Tag.Get(d.config.TagName) - keyName := f.Name + tagValue, _ := getTagValue(f, d.config.TagName) + keyName := d.config.MapFieldName(f.Name) if tagValue == "" && d.config.IgnoreUntaggedFields { continue @@ -1070,6 +1156,9 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re // If Squash is set in the config, we squash the field down. squash := d.config.Squash && v.Kind() == reflect.Struct && f.Anonymous + // If Deep is set in the config, set as default value. + deep := d.config.Deep + v = dereferencePtrToStructIfNeeded(v, d.config.TagName) // Determine the name of the key in the map @@ -1078,12 +1167,12 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re continue } // If "omitempty" is specified in the tag, it ignores empty values. - if strings.Index(tagValue[index+1:], "omitempty") != -1 && isEmptyValue(v) { + if strings.Contains(tagValue[index+1:], "omitempty") && isEmptyValue(v) { continue } // If "omitzero" is specified in the tag, it ignores zero values. - if strings.Index(tagValue[index+1:], "omitzero") != -1 && v.IsZero() { + if strings.Contains(tagValue[index+1:], "omitzero") && v.IsZero() { continue } @@ -1103,7 +1192,7 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re ) } } else { - if strings.Index(tagValue[index+1:], "remain") != -1 { + if strings.Contains(tagValue[index+1:], "remain") { if v.Kind() != reflect.Map { return newDecodeError( name+"."+f.Name, @@ -1118,6 +1207,9 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re continue } } + + deep = deep || strings.Contains(tagValue[index+1:], "deep") + if keyNameTagValue := tagValue[:index]; keyNameTagValue != "" { keyName = keyNameTagValue } @@ -1164,6 +1256,41 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re valMap.SetMapIndex(reflect.ValueOf(keyName), vMap) } + case reflect.Slice: + if deep { + var childType reflect.Type + switch v.Type().Elem().Kind() { + case reflect.Struct: + childType = reflect.TypeOf(map[string]any{}) + default: + childType = v.Type().Elem() + } + + sType := reflect.SliceOf(childType) + + addrVal := reflect.New(sType) + + vSlice := reflect.MakeSlice(sType, v.Len(), v.Cap()) + + if v.Len() > 0 { + reflect.Indirect(addrVal).Set(vSlice) + + err := d.decode(keyName, v.Interface(), reflect.Indirect(addrVal)) + if err != nil { + return err + } + } + + vSlice = reflect.Indirect(addrVal) + + valMap.SetMapIndex(reflect.ValueOf(keyName), vSlice) + + break + } + + // When deep mapping is not needed, fallthrough to normal copy + fallthrough + default: valMap.SetMapIndex(reflect.ValueOf(keyName), v) } @@ -1471,7 +1598,10 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e remain := false // We always parse the tags cause we're looking for other tags too - tagParts := strings.Split(fieldType.Tag.Get(d.config.TagName), ",") + tagParts := getTagParts(fieldType, d.config.TagName) + if len(tagParts) == 0 { + tagParts = []string{""} + } for _, tag := range tagParts[1:] { if tag == d.config.SquashTagOption { squash = true @@ -1492,6 +1622,18 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e if !fieldVal.IsNil() { structs = append(structs, fieldVal.Elem().Elem()) } + case reflect.Ptr: + if fieldVal.Type().Elem().Kind() == reflect.Struct { + if fieldVal.IsNil() { + fieldVal.Set(reflect.New(fieldVal.Type().Elem())) + } + structs = append(structs, fieldVal.Elem()) + } else { + errs = append(errs, newDecodeError( + name+"."+fieldType.Name, + fmt.Errorf("unsupported type for squashed pointer: %s", fieldVal.Type().Elem().Kind()), + )) + } default: errs = append(errs, newDecodeError( name+"."+fieldType.Name, @@ -1516,13 +1658,15 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e field, fieldValue := f.field, f.val fieldName := field.Name - tagValue := field.Tag.Get(d.config.TagName) + tagValue, _ := getTagValue(field, d.config.TagName) if tagValue == "" && d.config.IgnoreUntaggedFields { continue } tagValue = strings.SplitN(tagValue, ",", 2)[0] if tagValue != "" { fieldName = tagValue + } else { + fieldName = d.config.MapFieldName(fieldName) } rawMapKey := reflect.ValueOf(fieldName) @@ -1605,8 +1749,14 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e } sort.Strings(keys) + // Improve error message when name is empty by showing the target struct type + // in the case where it is empty for embedded structs. + errorName := name + if errorName == "" { + errorName = val.Type().String() + } errs = append(errs, newDecodeError( - name, + errorName, fmt.Errorf("has invalid keys: %s", strings.Join(keys, ", ")), )) } @@ -1692,7 +1842,7 @@ func isStructTypeConvertibleToMap(typ reflect.Type, checkMapstructureTags bool, if f.PkgPath == "" && !checkMapstructureTags { // check for unexported fields return true } - if checkMapstructureTags && f.Tag.Get(tagName) != "" { // check for mapstructure tags inside + if checkMapstructureTags && hasAnyTag(f, tagName) { // check for mapstructure tags inside return true } } @@ -1700,13 +1850,99 @@ func isStructTypeConvertibleToMap(typ reflect.Type, checkMapstructureTags bool, } func dereferencePtrToStructIfNeeded(v reflect.Value, tagName string) reflect.Value { - if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { + if v.Kind() != reflect.Ptr { + return v + } + + switch v.Elem().Kind() { + case reflect.Slice: + return v.Elem() + + case reflect.Struct: + deref := v.Elem() + derefT := deref.Type() + if isStructTypeConvertibleToMap(derefT, true, tagName) { + return deref + } + return v + + default: return v } - deref := v.Elem() - derefT := deref.Type() - if isStructTypeConvertibleToMap(derefT, true, tagName) { - return deref +} + +func hasAnyTag(field reflect.StructField, tagName string) bool { + _, ok := getTagValue(field, tagName) + return ok +} + +func getTagParts(field reflect.StructField, tagName string) []string { + tagValue, ok := getTagValue(field, tagName) + if !ok { + return nil } - return v + return strings.Split(tagValue, ",") +} + +func getTagValue(field reflect.StructField, tagName string) (string, bool) { + for _, name := range splitTagNames(tagName) { + if tag := field.Tag.Get(name); tag != "" { + return tag, true + } + } + return "", false +} + +func splitTagNames(tagName string) []string { + if tagName == "" { + return []string{"mapstructure"} + } + parts := strings.Split(tagName, ",") + result := make([]string, 0, len(parts)) + + for _, name := range parts { + name = strings.TrimSpace(name) + if name != "" { + result = append(result, name) + } + } + + return result +} + +// unmarshalerType is cached for performance +var unmarshalerType = reflect.TypeOf((*Unmarshaler)(nil)).Elem() + +// getUnmarshaler checks if the value implements Unmarshaler and returns +// the Unmarshaler and a boolean indicating if it was found. It handles both +// pointer and value receivers. +func getUnmarshaler(val reflect.Value) (Unmarshaler, bool) { + // Skip invalid or nil values + if !val.IsValid() { + return nil, false + } + + switch val.Kind() { + case reflect.Pointer, reflect.Interface: + if val.IsNil() { + return nil, false + } + } + + // Check pointer receiver first (most common case) + if val.CanAddr() { + ptrVal := val.Addr() + // Quick check: if no methods, can't implement any interface + if ptrVal.Type().NumMethod() > 0 && ptrVal.Type().Implements(unmarshalerType) { + return ptrVal.Interface().(Unmarshaler), true + } + } + + // Check value receiver + // Quick check: if no methods, can't implement any interface + if val.Type().NumMethod() > 0 && val.CanInterface() && val.Type().Implements(unmarshalerType) { + return val.Interface().(Unmarshaler), true + } + + return nil, false } diff --git a/vendor/modules.txt b/vendor/modules.txt index c76c0191..b81741dd 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -310,8 +310,8 @@ github.com/buildpacks/imgutil/layout github.com/buildpacks/imgutil/layout/sparse github.com/buildpacks/imgutil/local github.com/buildpacks/imgutil/remote -# github.com/buildpacks/lifecycle v0.21.7 -## explicit; go 1.26.1 +# github.com/buildpacks/lifecycle v0.21.8 +## explicit; go 1.26.2 github.com/buildpacks/lifecycle/api github.com/buildpacks/lifecycle/archive github.com/buildpacks/lifecycle/auth @@ -426,8 +426,8 @@ github.com/containerd/errdefs/pkg/internal/cause # github.com/containerd/log v0.1.0 ## explicit; go 1.20 github.com/containerd/log -# github.com/containerd/platforms v1.0.0-rc.2 -## explicit; go 1.20 +# github.com/containerd/platforms v1.0.0-rc.4 +## explicit; go 1.24 github.com/containerd/platforms # github.com/containerd/stargz-snapshotter/estargz v0.18.2 ## explicit; go 1.24.0 @@ -616,7 +616,7 @@ github.com/go-toolsmith/strparse # github.com/go-toolsmith/typep v1.1.0 ## explicit; go 1.16 github.com/go-toolsmith/typep -# github.com/go-viper/mapstructure/v2 v2.4.0 +# github.com/go-viper/mapstructure/v2 v2.5.0 ## explicit; go 1.18 github.com/go-viper/mapstructure/v2 github.com/go-viper/mapstructure/v2/internal/errors @@ -1083,7 +1083,7 @@ github.com/mitchellh/ioprogress # github.com/mitchellh/mapstructure v1.5.0 ## explicit; go 1.14 github.com/mitchellh/mapstructure -# github.com/moby/buildkit v0.28.1 +# github.com/moby/buildkit v0.29.0 ## explicit; go 1.25.5 github.com/moby/buildkit/frontend/dockerfile/command github.com/moby/buildkit/frontend/dockerfile/instructions