diff --git a/go.mod b/go.mod index f3f38d8..7d8a87f 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/go-openapi/swag/jsonutils v0.29.1 github.com/go-openapi/swag/stringutils v0.29.1 github.com/go-openapi/swag/typeutils v0.29.1 - github.com/go-openapi/validate v0.26.3 + github.com/go-openapi/validate v0.26.4 github.com/obalunenko/getenv v1.15.0 github.com/stretchr/testify v1.12.1 ) @@ -19,17 +19,17 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/analysis v0.26.0 // indirect + github.com/go-openapi/analysis v0.26.1 // indirect github.com/go-openapi/jsonpointer v1.0.0 // indirect github.com/go-openapi/jsonreference v1.0.0 // indirect github.com/go-openapi/loads v0.25.1 // indirect github.com/go-openapi/runtime/server-middleware v0.33.1 // indirect - github.com/go-openapi/spec v0.22.9 // indirect - github.com/go-openapi/swag/fileutils v0.28.0 // indirect - github.com/go-openapi/swag/loading v0.28.0 // indirect - github.com/go-openapi/swag/mangling v0.28.0 // indirect + github.com/go-openapi/spec v0.22.10 // indirect + github.com/go-openapi/swag/fileutils v0.29.1 // indirect + github.com/go-openapi/swag/loading v0.29.1 // indirect + github.com/go-openapi/swag/mangling v0.29.1 // indirect github.com/go-openapi/swag/pools v0.29.1 // indirect - github.com/go-openapi/swag/yamlutils v0.28.0 // indirect + github.com/go-openapi/swag/yamlutils v0.29.1 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/oklog/ulid/v2 v2.1.2 // indirect diff --git a/go.sum b/go.sum index b6dddfe..d07198c 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/analysis v0.26.0 h1:1xECln1iMMmQnTjgcknC1vi1hA4KISt6IHpSwnqcuwI= -github.com/go-openapi/analysis v0.26.0/go.mod h1:40gERFi/2dyXA1FaqRRLxkv1IlC6X+GPDNd1xrYAjZE= +github.com/go-openapi/analysis v0.26.1 h1:BqYuDaQiFflcgPWGDQc7niUFE2pHTVq1H3iwDsRYMqM= +github.com/go-openapi/analysis v0.26.1/go.mod h1:E2siwFrz00/Z1sifwhy63h+hMkg0USofwD4gYPYeQME= github.com/go-openapi/errors v0.22.8 h1:oP7sW7TWc3wFFjrzzj0nI83H2qMBkNjNfSd+XRejk/I= github.com/go-openapi/errors v0.22.8/go.mod h1:BuUoHcYrU6E7V9gfj1I5wLQqgtIHnup/alXZ8KdgQ0w= github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s= @@ -19,36 +19,36 @@ github.com/go-openapi/runtime v0.33.1 h1:jCvhI+wAdsn29byy+RgcPcg+j39YT6E304QOE/W github.com/go-openapi/runtime v0.33.1/go.mod h1:Dl5SMVRnJz+d8bX6Y1zxy0QKpqe/ysvVeUEh1nCpEZ4= github.com/go-openapi/runtime/server-middleware v0.33.1 h1:IAeKbwWnBnpsYTpuPVS8t73ZrPpKvRZnK2iJ2KJGUV0= github.com/go-openapi/runtime/server-middleware v0.33.1/go.mod h1:2Gej5fDxqeJxY+w38vxXYW0BgFASfgBsJ5rXwN1Fseg= -github.com/go-openapi/spec v0.22.9 h1:/vKIFDcGKp0ktZWGbym/tJEWbk6/XOEmAVU0kqKMH+w= -github.com/go-openapi/spec v0.22.9/go.mod h1:b/mNUYIOQOyIiUzUzXEE8xzyZqf93KvM9hQGP91yfl0= +github.com/go-openapi/spec v0.22.10 h1:5cp1dq++t4U/4WCg6f1wqReZowUrJ5kl8Eri4SMl51s= +github.com/go-openapi/spec v0.22.10/go.mod h1:aWRr+Ntv5tHoMQo0C1slTNLFo1FOYdZXFlUqViCN7yM= github.com/go-openapi/strfmt v0.27.0 h1:kbcTeaD9TXuXD0hhMXzuYa1sdTo6+dWGvwjW93E80IM= github.com/go-openapi/strfmt v0.27.0/go.mod h1:s/qhDqfY72irigXUGJmtgid2Rm+3tnz3k8hZaRmvWYc= github.com/go-openapi/swag/conv v0.29.1 h1:AC4Eh/5c/eUDOUCzzsRC9ghmFgOSBHeRMGIngY0ZUGA= github.com/go-openapi/swag/conv v0.29.1/go.mod h1:S1X7/ZrBEZOC0Wc8AGxjbcGS92l3WEjA7aPtpl+RaqM= -github.com/go-openapi/swag/fileutils v0.28.0 h1:Z04XWQD7R8Eq+7GnOrjovBxPPmZzsS4gt2H2GPGIViU= -github.com/go-openapi/swag/fileutils v0.28.0/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8= +github.com/go-openapi/swag/fileutils v0.29.1 h1:ZcPzMceVhU1WPbK6N1G6sNQKdd1CWJlf3cA08UHuoM0= +github.com/go-openapi/swag/fileutils v0.29.1/go.mod h1:/wofKYckbtRl2p3+EwQsosie5CT1B38+dQ+PS579BzI= github.com/go-openapi/swag/jsonutils v0.29.1 h1:AFCxs0eQZ24/QyfhVHM2t49rMz7Vv3XCsZQI6yrNy+c= github.com/go-openapi/swag/jsonutils v0.29.1/go.mod h1:u3+sCfJpttDpcmS5kpm0yxL6GK0eWgODsx8Yw8fcqNM= github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.1 h1:BiiXE31Bx9SfpsMmOQj5KYpUhTZBpLVriVhJDuLuY2o= github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.1/go.mod h1:julgTUKZ9/D0j6O7GKajmRs+812FWxQg/mMpGunWSjg= -github.com/go-openapi/swag/loading v0.28.0 h1:td8QZdZC9MIYGGSnSPKShKiK22I2tU5UQvuUhIBPRLU= -github.com/go-openapi/swag/loading v0.28.0/go.mod h1:rXB0QiQX5mMveXEA7ouM4KiiM9jVJe4K6BVbwhD1M4k= -github.com/go-openapi/swag/mangling v0.28.0 h1:pH8eyeNO9SLYsTMWJrurnNfKmDa28XrlA+HePVD53VM= -github.com/go-openapi/swag/mangling v0.28.0/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w= +github.com/go-openapi/swag/loading v0.29.1 h1:FCv5fG8UhTdDJa2R7w+5O9Ekpcbw7tt0nFWvmDKGBjc= +github.com/go-openapi/swag/loading v0.29.1/go.mod h1:N0ESuem4p2oedKal8EJhciqnJ9Q9Wmt83L1CRB3Fouw= +github.com/go-openapi/swag/mangling v0.29.1 h1:lHALtvYCdxVnRl4GrHmFPwfBTZYIObqdGNSKyu/8D6I= +github.com/go-openapi/swag/mangling v0.29.1/go.mod h1:SAop9pB7PUjQ/CGCNf/JmCKTRK+GDO+RqE9UHqC/N6s= github.com/go-openapi/swag/pools v0.29.1 h1:NRogYxdEW9SjRM4mkAOji9iefO4MRXq3p/ZJcoQbUKg= github.com/go-openapi/swag/pools v0.29.1/go.mod h1:leDcaghjkRAhCuCRv9NfJU5f0mjoU3cT/XZObhMk3pc= github.com/go-openapi/swag/stringutils v0.29.1 h1:1ykunK7iJQk1uOO7+oUH1ukbsK85fFCOiCFMOVSY+F0= github.com/go-openapi/swag/stringutils v0.29.1/go.mod h1:7fSqZ+z8Qc0tOfAAK0jVa5qFGrnIlRi6n7NeGGrr1vc= github.com/go-openapi/swag/typeutils v0.29.1 h1:Nzv9nhnlLCRBPQqfOX+7lB6Guju370or8StT+lIOf6M= github.com/go-openapi/swag/typeutils v0.29.1/go.mod h1:hxpgDZJVBkBsi/d3MIUosafoFdE5exaQRmVp0zwu3YE= -github.com/go-openapi/swag/yamlutils v0.28.0 h1:TV3JXH6DS46KUroDtMLAYHGkdWf5VDq3wVWFirmzROY= -github.com/go-openapi/swag/yamlutils v0.28.0/go.mod h1:x0q/yndZHEgk9Rx3DyDqzFUmHy55KTvIZldvF2dTJXs= +github.com/go-openapi/swag/yamlutils v0.29.1 h1:69w3tsBajm7MR/fejLy7HD/3J68Ys1SeeZMEzZ3w2sk= +github.com/go-openapi/swag/yamlutils v0.29.1/go.mod h1:rgsp3vT/QdWzKwn43CigDwjOGIenPyTZMKnxEM8jZOA= github.com/go-openapi/testify/enable/yaml/v2 v2.6.1 h1:Jm+/ze2rMtbD98yen92AhATGLGREDYXG56Xr4gMjEtE= github.com/go-openapi/testify/enable/yaml/v2 v2.6.1/go.mod h1:YDPnwCRDu38/oJBVMBVXOUDiJ9cIeBHWvfImHaXqnv4= -github.com/go-openapi/testify/v2 v2.6.1 h1:6CNJhTjMzgaeaH8WhshcsZNPIvRemiOcFpU7seO/y7Q= -github.com/go-openapi/testify/v2 v2.6.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= -github.com/go-openapi/validate v0.26.3 h1:OkfZgLvLDnGP2hrRGD+42WBiPWWkoHomTJ+IVI+KaDc= -github.com/go-openapi/validate v0.26.3/go.mod h1:7DOOa4raU6NRe7A8VQSKbm3VcuUIioREYHFt+er9Sk8= +github.com/go-openapi/testify/v2 v2.7.0 h1:bycOreEj6wfBvijg3YFogZ/sFjTCDmQnwSodSzHa3X8= +github.com/go-openapi/testify/v2 v2.7.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/go-openapi/validate v0.26.4 h1:MoiZYinqehNWMxPfKsyo8kxs7uYoFAf6k+4QfTHRvBI= +github.com/go-openapi/validate v0.26.4/go.mod h1:qRkL7p9Dk183ZK3aZ6F+cekhKCioj9NLnFLnTNuPcXM= 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/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= diff --git a/vendor/github.com/go-openapi/analysis/.codecov.yml b/vendor/github.com/go-openapi/analysis/.codecov.yml index a5ba8e9..75d4a08 100644 --- a/vendor/github.com/go-openapi/analysis/.codecov.yml +++ b/vendor/github.com/go-openapi/analysis/.codecov.yml @@ -7,3 +7,8 @@ coverage: patch: default: target: 80% + +ignore: + - "testdata" + - "internal/antest" + - "internal/testintegration" diff --git a/vendor/github.com/go-openapi/analysis/.golangci.yml b/vendor/github.com/go-openapi/analysis/.golangci.yml index 0d7baa1..8488e97 100644 --- a/vendor/github.com/go-openapi/analysis/.golangci.yml +++ b/vendor/github.com/go-openapi/analysis/.golangci.yml @@ -10,6 +10,7 @@ linters: - gomodguard - gomodguard_v2 - exhaustruct + - exhaustruct_v5 - nlreturn - nonamedreturns - noinlineerr diff --git a/vendor/github.com/go-openapi/analysis/README.md b/vendor/github.com/go-openapi/analysis/README.md index 2a90462..abb0de9 100644 --- a/vendor/github.com/go-openapi/analysis/README.md +++ b/vendor/github.com/go-openapi/analysis/README.md @@ -5,7 +5,7 @@ -[![Release][release-badge]][release-url] [![Go Report Card][gocard-badge]][gocard-url] [![CodeFactor Grade][codefactor-badge]][codefactor-url] [![License][license-badge]][license-url] +[![Release][release-badge]][release-url] [![CodeFactor Grade][codefactor-badge]][codefactor-url] [![License][license-badge]][license-url] [![GoDoc][godoc-badge]][godoc-url] [![Discord Channel][discord-badge]][discord-url] [![go version][goversion-badge]][goversion-url] ![Top language][top-badge] ![Commits since latest release][commits-badge] @@ -102,8 +102,6 @@ Maintainers can cut a new release by either: [release-badge]: https://badge.fury.io/gh/go-openapi%2Fanalysis.svg [release-url]: https://badge.fury.io/gh/go-openapi%2Fanalysis -[gocard-badge]: https://goreportcard.com/badge/github.com/go-openapi/analysis -[gocard-url]: https://goreportcard.com/report/github.com/go-openapi/analysis [codefactor-badge]: https://img.shields.io/codefactor/grade/github/go-openapi/analysis [codefactor-url]: https://www.codefactor.io/repository/github/go-openapi/analysis diff --git a/vendor/github.com/go-openapi/analysis/analyzer.go b/vendor/github.com/go-openapi/analysis/analyzer.go index 9eab8bd..63245a1 100644 --- a/vendor/github.com/go-openapi/analysis/analyzer.go +++ b/vendor/github.com/go-openapi/analysis/analyzer.go @@ -30,6 +30,15 @@ type referenceAnalysis struct { parameterItems map[string]spec.Ref allRefs map[string]spec.Ref pathItems map[string]spec.Ref + + // unmappedRefs holds the $ref found under keywords the Swagger 2.0 model does not map, which + // land in [spec.Schema.ExtraProps] as raw JSON: propertyNames, contains, if/then/else, $defs. + // + // They are kept apart from allRefs and schemas on purpose. Flatten needs them to import their + // target and rewrite the pointer; every other consumer of this analysis - AllRefs, + // AllReferences, AllDefinitionReferences and what go-swagger builds on them - addresses schemas + // through the model, and a key naming a raw JSON node is of no use there. + unmappedRefs map[string]spec.Ref } func (r *referenceAnalysis) addRef(key string, ref spec.Ref) { @@ -48,6 +57,11 @@ func (r *referenceAnalysis) addItemsRef(key string, items *spec.Items, location } } +// addUnmappedRef records a $ref held by a keyword the model does not map. +func (r *referenceAnalysis) addUnmappedRef(key string, ref spec.Ref) { + r.unmappedRefs["#"+key] = ref +} + func (r *referenceAnalysis) addSchemaRef(key string, ref SchemaRef) { r.schemas["#"+key] = ref.Schema.Ref r.addRef(key, ref.Schema.Ref) @@ -739,6 +753,7 @@ func (s *Spec) reset() { s.references.headerItems = make(map[string]spec.Ref, allocLargeMap) s.references.parameterItems = make(map[string]spec.Ref, allocLargeMap) s.references.allRefs = make(map[string]spec.Ref, allocLargeMap) + s.references.unmappedRefs = make(map[string]spec.Ref, allocSmallMap) s.patterns.parameters = make(map[string]string, allocLargeMap) s.patterns.headers = make(map[string]string, allocLargeMap) s.patterns.items = make(map[string]string, allocLargeMap) @@ -972,6 +987,42 @@ func (s *Spec) analyzeResponse(prefix string, k int, res spec.Response) { } } +// analyzeUnmapped records the $ref held by the keywords of a schema that the Swagger 2.0 model +// does not map, which json.Unmarshal leaves in ExtraProps as raw JSON. +// +// The keys it produces address the node holding the $ref, so "#/definitions/deep/propertyNames" +// or "#/definitions/deep/if/anyOf/0". [replace.UpdateRef] writes to them through the same +// jsonpointer call every other key goes through. +func (s *Spec) analyzeUnmapped(prefix string, extra map[string]any) { + for key := range extra { + s.analyzeUnmappedNode(slashpath.Join(prefix, jsonpointer.Escape(key)), extra[key]) + } +} + +func (s *Spec) analyzeUnmappedNode(refURI string, node any) { + switch value := node.(type) { + case map[string]any: + if raw, ok := value["$ref"].(string); ok { + ref, err := spec.NewRef(raw) + if err != nil { + return // a string under a "$ref" key is not necessarily a reference + } + + s.references.addUnmappedRef(refURI, ref) + + return // a $ref makes its siblings irrelevant + } + + for key := range value { + s.analyzeUnmappedNode(slashpath.Join(refURI, jsonpointer.Escape(key)), value[key]) + } + case []any: + for i := range value { + s.analyzeUnmappedNode(slashpath.Join(refURI, strconv.Itoa(i)), value[i]) + } + } +} + func (s *Spec) analyzeSchema(name string, schema *spec.Schema, prefix string) { refURI := slashpath.Join(prefix, jsonpointer.Escape(name)) schRef := SchemaRef{ @@ -995,6 +1046,8 @@ func (s *Spec) analyzeSchema(name string, schema *spec.Schema, prefix string) { s.enums.addSchemaEnum(refURI, schema.Enum) } + s.analyzeUnmapped(refURI, schema.ExtraProps) + for k, v := range schema.Definitions { s.analyzeSchema(k, &v, slashpath.Join(refURI, "definitions")) } diff --git a/vendor/github.com/go-openapi/analysis/flatten.go b/vendor/github.com/go-openapi/analysis/flatten.go index c90456f..2b7371f 100644 --- a/vendor/github.com/go-openapi/analysis/flatten.go +++ b/vendor/github.com/go-openapi/analysis/flatten.go @@ -5,6 +5,7 @@ package analysis import ( "log" + "maps" "path" "slices" "sort" @@ -179,18 +180,20 @@ func normalizeRef(opts *FlattenOpts) error { debugLog("normalizeRef") altered := false - for k, w := range opts.Spec.references.allRefs { - if !strings.HasPrefix(w.String(), opts.BasePath+definitionsPath) { // may be a mix of / and \, depending on OS - continue - } + for _, refs := range []map[string]spec.Ref{opts.Spec.references.allRefs, opts.Spec.references.unmappedRefs} { + for k, w := range refs { + if !strings.HasPrefix(w.String(), opts.BasePath+definitionsPath) { // may be a mix of / and \, depending on OS + continue + } - altered = true - debugLog("stripping absolute path for: %s", w.String()) + altered = true + debugLog("stripping absolute path for: %s", w.String()) - // strip the base path from definition - if err := replace.UpdateRef(opts.Swagger(), k, - spec.MustCreateRef(path.Join(definitionsPath, path.Base(w.String())))); err != nil { - return err + // strip the base path from definition + if err := replace.UpdateRef(opts.Swagger(), k, + spec.MustCreateRef(path.Join(definitionsPath, path.Base(w.String())))); err != nil { + return err + } } } @@ -276,6 +279,10 @@ func removeUnusedSinglePass(opts *FlattenOpts) (hasRemoved bool) { delete(expected, k) } + for _, ref := range opts.Spec.references.unmappedRefs { + delete(expected, ref.String()) + } + for k := range expected { hasRemoved = true debugLog("removing unused definition %s", path.Base(k)) @@ -327,9 +334,11 @@ func importNewRef(entry sortref.RefRevIdx, refStr string, opts *FlattenOpts) err partialAnalyzer.analyzeSchema("", sch, "/") // now rewrite those refs with rebase - for key, ref := range partialAnalyzer.references.allRefs { - if err := replace.UpdateRef(sch, key, spec.MustCreateRef(normalize.RebaseRef(entry.Ref.String(), ref.String()))); err != nil { - return ErrRewriteRef(key, entry.Ref.String(), err) + for _, refs := range []map[string]spec.Ref{partialAnalyzer.references.allRefs, partialAnalyzer.references.unmappedRefs} { + for key, ref := range refs { + if err := replace.UpdateRef(sch, key, spec.MustCreateRef(normalize.RebaseRef(entry.Ref.String(), ref.String()))); err != nil { + return ErrRewriteRef(key, entry.Ref.String(), err) + } } } @@ -374,10 +383,20 @@ func importNewRef(entry sortref.RefRevIdx, refStr string, opts *FlattenOpts) err // At every iteration, new remotes may be found when digging deeper: they are rebased to the current schema before being imported. // // This returns true when no more remote references can be found. +// importableRefs returns every $ref that flatten has to bring into the root document: the ones the +// model maps, and the ones held by keywords it does not. +func importableRefs(sp *Spec) map[string]spec.Ref { + refs := make(map[string]spec.Ref, len(sp.references.schemas)+len(sp.references.unmappedRefs)) + maps.Copy(refs, sp.references.schemas) + maps.Copy(refs, sp.references.unmappedRefs) + + return refs +} + func importExternalReferences(opts *FlattenOpts) (bool, error) { debugLog("importExternalReferences") - groupedRefs := sortref.ReverseIndex(opts.Spec.references.schemas, opts.BasePath) + groupedRefs := sortref.ReverseIndex(importableRefs(opts.Spec), opts.BasePath) sortedRefStr := make([]string, 0, len(groupedRefs)) if opts.flattenContext == nil { opts.flattenContext = newContext() diff --git a/vendor/github.com/go-openapi/analysis/internal/flatten/replace/replace.go b/vendor/github.com/go-openapi/analysis/internal/flatten/replace/replace.go index b4c0fdd..66cad8f 100644 --- a/vendor/github.com/go-openapi/analysis/internal/flatten/replace/replace.go +++ b/vendor/github.com/go-openapi/analysis/internal/flatten/replace/replace.go @@ -19,6 +19,7 @@ import ( const ( definitionsPath = "#/definitions" + jsonRef = "$ref" allocMediumMap = 64 ) @@ -217,6 +218,17 @@ func UpdateRef(sp any, key string, ref spec.Ref) error { switch refable := value.(type) { case *spec.Schema: refable.Ref = ref + case map[string]any: + // a keyword the Swagger 2.0 model does not map: the node is raw JSON, and the map it + // holds is the one in the document - writing to it reaches the document + refable[jsonRef] = ref.String() + case *any: + raw, ok := (*refable).(map[string]any) + if !ok { + return ErrNoSchemaWithRef(key, value) + } + + raw[jsonRef] = ref.String() case *spec.SchemaOrArray: if refable.Schema != nil { refable.Schema.Ref = ref diff --git a/vendor/github.com/go-openapi/analysis/revive.toml b/vendor/github.com/go-openapi/analysis/revive.toml new file mode 100644 index 0000000..eef9c17 --- /dev/null +++ b/vendor/github.com/go-openapi/analysis/revive.toml @@ -0,0 +1,18 @@ +# revive configuration used by codefactor.io. +# +# This is redundant with .golangci.yml, since revive is part of golangci-lint, +# but we need to configure it to remain consistent across reporting tools. + +ignore-generated-header = false +severity = "warning" +confidence = 0.8 +error-code = 0 +warning-code = 0 +enable-default-rules = true + +[rule.exported] +disabled = true +[rule.cyclomatic] +arguments = [25] +[rule.cognitive-complexity] +arguments = [25] diff --git a/vendor/github.com/go-openapi/spec/.golangci.yml b/vendor/github.com/go-openapi/spec/.golangci.yml index 9d27331..3f3b5af 100644 --- a/vendor/github.com/go-openapi/spec/.golangci.yml +++ b/vendor/github.com/go-openapi/spec/.golangci.yml @@ -9,6 +9,7 @@ linters: - gomodguard - gomodguard_v2 - exhaustruct + - exhaustruct_v5 - nlreturn - nonamedreturns - noinlineerr diff --git a/vendor/github.com/go-openapi/spec/CONTRIBUTORS.md b/vendor/github.com/go-openapi/spec/CONTRIBUTORS.md index 12fd069..5871da5 100644 --- a/vendor/github.com/go-openapi/spec/CONTRIBUTORS.md +++ b/vendor/github.com/go-openapi/spec/CONTRIBUTORS.md @@ -4,12 +4,12 @@ | Total Contributors | Total Contributions | | --- | --- | -| 38 | 403 | +| 38 | 414 | | Username | All Time Contribution Count | All Commits | | --- | --- | --- | | @casualjim | 191 | | -| @fredbi | 101 | | +| @fredbi | 112 | | | @pytlesk4 | 26 | | | @kul-amr | 10 | | | @keramix | 10 | | diff --git a/vendor/github.com/go-openapi/spec/README.md b/vendor/github.com/go-openapi/spec/README.md index 7c96eb9..9188f83 100644 --- a/vendor/github.com/go-openapi/spec/README.md +++ b/vendor/github.com/go-openapi/spec/README.md @@ -5,7 +5,7 @@ -[![Release][release-badge]][release-url] [![Go Report Card][gocard-badge]][gocard-url] [![CodeFactor Grade][codefactor-badge]][codefactor-url] [![License][license-badge]][license-url] +[![Release][release-badge]][release-url] [![CodeFactor Grade][codefactor-badge]][codefactor-url] [![License][license-badge]][license-url] [![GoDoc][godoc-badge]][godoc-url] [![Discord Channel][discord-badge]][discord-url] [![go version][goversion-badge]][goversion-url] ![Top language][top-badge] ![Commits since latest release][commits-badge] @@ -120,8 +120,6 @@ Maintainers can cut a new release by either: [gomod-badge]: https://badge.fury.io/go/github.com%2Fgo-openapi%2Fspec.svg [gomod-url]: https://badge.fury.io/go/github.com%2Fgo-openapi%2Fspec -[gocard-badge]: https://goreportcard.com/badge/github.com/go-openapi/spec -[gocard-url]: https://goreportcard.com/report/github.com/go-openapi/spec [codefactor-badge]: https://img.shields.io/codefactor/grade/github/go-openapi/spec [codefactor-url]: https://www.codefactor.io/repository/github/go-openapi/spec diff --git a/vendor/github.com/go-openapi/spec/expander.go b/vendor/github.com/go-openapi/spec/expander.go index 00eb5b5..c9ffed5 100644 --- a/vendor/github.com/go-openapi/spec/expander.go +++ b/vendor/github.com/go-openapi/spec/expander.go @@ -4,8 +4,12 @@ package spec import ( + "cmp" "encoding/json" "fmt" + "iter" + "maps" + "slices" "github.com/go-openapi/swag/loading" ) @@ -109,7 +113,8 @@ func ExpandSpec(spec *Swagger, options *ExpandOptions) error { specBasePath := options.RelativeBase if !options.SkipSchemas { - for key, definition := range spec.Definitions { + for key := range sortedKeys(spec.Definitions) { + definition := spec.Definitions[key] parentRefs := make([]string, 0, smallPrealloc) parentRefs = append(parentRefs, "#/definitions/"+key) @@ -123,7 +128,7 @@ func ExpandSpec(spec *Swagger, options *ExpandOptions) error { } } - for key := range spec.Parameters { + for key := range sortedKeys(spec.Parameters) { parameter := spec.Parameters[key] if err := expandParameterOrResponse(¶meter, resolver, specBasePath); resolver.shouldStopOnError(err) { return err @@ -131,7 +136,7 @@ func ExpandSpec(spec *Swagger, options *ExpandOptions) error { spec.Parameters[key] = parameter } - for key := range spec.Responses { + for key := range sortedKeys(spec.Responses) { response := spec.Responses[key] if err := expandParameterOrResponse(&response, resolver, specBasePath); resolver.shouldStopOnError(err) { return err @@ -140,7 +145,7 @@ func ExpandSpec(spec *Swagger, options *ExpandOptions) error { } if spec.Paths != nil { - for key := range spec.Paths.Paths { + for key := range sortedKeys(spec.Paths.Paths) { pth := spec.Paths.Paths[key] if err := expandPathItem(&pth, resolver, specBasePath); resolver.shouldStopOnError(err) { return err @@ -278,6 +283,38 @@ func expandItems(target Schema, parentRefs []string, resolver *schemaLoader, bas return &target, nil } +// sortedKeys walks the keys of a map in a fixed order. +// +// Expansion inlines the first branch that reaches a cycle and leaves a $ref on the others, so +// the order the walk visits siblings in decides which node ends up holding the $ref. Ranging a +// map straight gives that decision to Go's map iteration, and the same document then expands +// differently from one run to the next - see go-openapi/spec#93. +// +// A map of fewer than two keys has only one order, so it is yielded without sorting: schemata +// with a single property or definition are most of what a walk of a large document visits, and +// the slice this would otherwise allocate is paid at every node. +func sortedKeys[K cmp.Ordered, V any](m map[K]V) iter.Seq[K] { + const alreadyOrdered = 2 // a map of fewer keys than this has only one order + + return func(yield func(K) bool) { + if len(m) < alreadyOrdered { + for key := range m { + yield(key) + + return + } + + return + } + + for _, key := range slices.Sorted(maps.Keys(m)) { + if !yield(key) { + return + } + } + } +} + //nolint:gocognit,gocyclo,cyclop // complex but well-tested $ref expansion logic; refactoring deferred to dedicated PR func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, basePath string) (*Schema, error) { if err := resolver.context.countNode(); err != nil { @@ -312,7 +349,9 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba return &target, nil } - for k := range target.Definitions { + rebaseExtraRefs(target.ExtraProps, resolver, basePath) + + for k := range sortedKeys(target.Definitions) { tt, err := expandSchema(target.Definitions[k], parentRefs, resolver, basePath) if resolver.shouldStopOnError(err) { return &target, err @@ -370,7 +409,7 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba } } - for k := range target.Properties { + for k := range sortedKeys(target.Properties) { t, err := expandSchema(target.Properties[k], parentRefs, resolver, basePath) if resolver.shouldStopOnError(err) { return &target, err @@ -390,7 +429,7 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba } } - for k := range target.PatternProperties { + for k := range sortedKeys(target.PatternProperties) { t, err := expandSchema(target.PatternProperties[k], parentRefs, resolver, basePath) if resolver.shouldStopOnError(err) { return &target, err @@ -400,7 +439,7 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba } } - for k := range target.Dependencies { + for k := range sortedKeys(target.Dependencies) { if target.Dependencies[k].Schema != nil { t, err := expandSchema(*target.Dependencies[k].Schema, parentRefs, resolver, basePath) if resolver.shouldStopOnError(err) { @@ -424,6 +463,67 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba return &target, nil } +// rebaseExtraRefs rewrites the $ref held by keywords this model does not map, so that +// they still point at their target once the schema is inlined into another document. +// +// expandSchema walks the fields of [Schema] and stops there. A keyword the Swagger 2.0 +// model predates - propertyNames, contains, if/then/else, $defs - lands in ExtraProps as +// raw JSON, and a $ref inside it is copied into the root verbatim: "#/definitions/leaf" +// then names a definition of the root document instead of the one it came from. +// +// Rebasing makes the pointer correct. It does not expand it, and it does not make the +// expanded document self-contained: a $ref that came from another document keeps pointing +// there. +func rebaseExtraRefs(extra map[string]any, resolver *schemaLoader, basePath string) { + for key := range extra { + rebaseRawRefs(extra[key], resolver, basePath) + } +} + +// rebaseRawRefs walks raw JSON and rebases every "$ref" string value it finds. +func rebaseRawRefs(node any, resolver *schemaLoader, basePath string) { + switch value := node.(type) { + case map[string]any: + for key := range value { + if key == jsonRef { + if ref, ok := value[key].(string); ok { + if rebased, ok := rebaseRawRef(ref, resolver, basePath); ok { + value[key] = rebased + } + + continue + } + } + + rebaseRawRefs(value[key], resolver, basePath) + } + case []any: + for i := range value { + rebaseRawRefs(value[i], resolver, basePath) + } + } +} + +// rebaseRawRef resolves a $ref against basePath, then spells it relative to the document +// being expanded, like the SkipSchemas branch of [expandSchema] does for a mapped $ref. +// +// It reports false when the $ref is empty or does not parse: an unmapped keyword may hold +// any JSON, and a string under a "$ref" key is not necessarily a reference. +func rebaseRawRef(ref string, resolver *schemaLoader, basePath string) (string, bool) { + if ref == "" { + return "", false + } + + rebased, err := NewRef(normalizeURI(ref, basePath)) + if err != nil { + return "", false + } + + denormalized := denormalizeRef(&rebased, resolver.context.basePath, resolver.context.rootID) + + return denormalized.String(), true +} + func expandSchemaRef(target Schema, parentRefs []string, resolver *schemaLoader, basePath string) (*Schema, error) { // if a Ref is found, all sibling fields are skipped // Ref also changes the resolution scope of children expandSchema @@ -528,7 +628,7 @@ func expandOperation(op *Operation, resolver *schemaLoader, basePath string) err return err } - for code := range responses.StatusCodeResponses { + for code := range sortedKeys(responses.StatusCodeResponses) { response := responses.StatusCodeResponses[code] if err := expandParameterOrResponse(&response, resolver, basePath); resolver.shouldStopOnError(err) { return err diff --git a/vendor/github.com/go-openapi/spec/normalizer.go b/vendor/github.com/go-openapi/spec/normalizer.go index 68252dc..3ca3f73 100644 --- a/vendor/github.com/go-openapi/spec/normalizer.go +++ b/vendor/github.com/go-openapi/spec/normalizer.go @@ -56,6 +56,14 @@ func normalizeURI(refPath, base string) string { // copying fragment from ref to base baseURL.Fragment = refURL.Fragment + if baseURL.Scheme != "" && baseURL.Path != "" && !path.IsAbs(baseURL.Path) { + // a base that carries no path of its own (e.g. "smb://host") leaves the join relative. + // Anchor it, because a relative path under a scheme does not survive rendering: it is + // either promoted to a host component, which is not even guaranteed to be a legal one + // ("a://some file.json" no longer parses), or read back as absolute anyway. + baseURL.Path = path.Join("/", baseURL.Path) + } + return baseURL.String() } @@ -82,23 +90,45 @@ func denormalizeRef(ref *Ref, originalRelativeBase, id string) Ref { } if id != "" { - idBaseURL, err := parseURL(id) - if err == nil { // if the schema id is not usable as a URI, ignore it - if ref, ok := rebase(ref, idBaseURL, true); ok { // rebase, but keep references to root unchanged (do not want $ref: "") + if _, err := parseURL(id); err == nil { // if the schema id is not usable as a URI, ignore it + // rebase, but keep references to root unchanged (do not want $ref: "") + if ref, ok := rebase(ref, canonicalURL(id), true); ok { // $ref relative to the ID of the schema in the root document return ref } } } - originalRelativeBaseURL, _ := parseURL(originalRelativeBase) - - r, _ := rebase(ref, originalRelativeBaseURL, false) + r, _ := rebase(ref, canonicalURL(originalRelativeBase), false) return r } -func rebase(ref *Ref, v *url.URL, notEqual bool) (Ref, bool) { +// canonicalURL parses a URI the way a Ref does, so that rebase compares authorities that are +// spelled alike. +// +// Turning a URI into a Ref lower-cases the host and drops a default port, whereas the normalizer +// keeps the spelling it was handed: comparing the two spellings directly makes a $ref look like +// it belongs to another host, and leaves it absolute when it should have been rebased. +func canonicalURL(in string) *url.URL { + if ref, err := NewRef(in); err == nil { + return ref.GetURL() + } + + if u, err := parseURL(in); err == nil { + return u + } + + return &url.URL{} +} + +// rebase expresses a $ref relative to v, which is either the URI of the base document or +// the "id" that anchors the root schema. +// +// The two differ in what v stands for. An "id" anchors a namespace, so a $ref below it is +// expressed relative to the id itself. A base is a document, so a $ref is expressed relative +// to the folder that holds it, and only a $ref to that very document collapses to an empty $ref. +func rebase(ref *Ref, v *url.URL, isID bool) (Ref, bool) { var newBase url.URL u := ref.GetURL() @@ -118,26 +148,78 @@ func rebase(ref *Ref, v *url.URL, notEqual bool) (Ref, bool) { newBase.Fragment = u.Fragment - if after, ok := strings.CutPrefix(u.Path, docPath); ok { - newBase.Path = after - } else { + switch { + case isID: + if after, ok := cutPathPrefix(u.Path, docPath); ok { + newBase.Path = after + } else { + newBase.Path = strings.TrimPrefix(u.Path, v.Path) + } + + if newBase.Path == "" && newBase.Fragment == "" { + // do not want rebasing to end up in an empty $ref + return *ref, false + } + + case u.Path == docPath: + // the $ref points to the base document itself + newBase.Path = "" + + default: newBase.Path = strings.TrimPrefix(u.Path, v.Path) - } - if notEqual && newBase.Path == "" && newBase.Fragment == "" { - // do not want rebasing to end up in an empty $ref - return *ref, false + if newBase.Path == "" { + // the $ref points to the folder that holds the base, not to a document: + // a $ref with no path of its own would denote the base document instead + return *ref, false + } } if path.IsAbs(newBase.Path) { - // whenever we end up with an absolute path, specify the scheme and host + // whenever we end up with an absolute path, we render a whole URI newBase.Scheme = v.Scheme newBase.Host = v.Host + newBase.User = u.User + newBase.RawQuery = u.RawQuery + newBase.ForceQuery = u.ForceQuery + } else if u.RawQuery != v.RawQuery || u.ForceQuery != v.ForceQuery || !sameUserinfo(u.User, v.User) { + // a relative $ref carries neither credentials nor query of its own: normalizing one + // applies those of the base. Leave the $ref absolute rather than have it resolve + // against a different query or different credentials. + return *ref, false } return MustCreateRef(newBase.String()), true } +// sameUserinfo compares the userinfo components of two URIs. +// +// An absent userinfo differs from an empty one: "file:///x" and "file://@/x" do not render alike. +func sameUserinfo(a, b *url.Userinfo) bool { + if a == nil || b == nil { + return a == b + } + + return a.String() == b.String() +} + +// cutPathPrefix is strings.CutPrefix, cutting on path separators only. +// +// "/base/spec.json" is a prefix of "/base/spec.json/x" but not of "/base/spec.json.orig": +// cutting the latter would leave ".orig" to be rebased against the wrong folder. +func cutPathPrefix(pth, prefix string) (string, bool) { + after, ok := strings.CutPrefix(pth, prefix) + if !ok { + return "", false + } + + if after == "" || strings.HasPrefix(after, "/") || strings.HasSuffix(prefix, "/") { + return after, true + } + + return "", false +} + // normalizeRef canonicalize a Ref, using a canonical relativeBase as its absolute anchor. func normalizeRef(ref *Ref, relativeBase string) *Ref { r := MustCreateRef(normalizeURI(ref.String(), relativeBase)) diff --git a/vendor/github.com/go-openapi/spec/normalizer_windows.go b/vendor/github.com/go-openapi/spec/normalizer_windows.go index 61515c9..a439f43 100644 --- a/vendor/github.com/go-openapi/spec/normalizer_windows.go +++ b/vendor/github.com/go-openapi/spec/normalizer_windows.go @@ -1,4 +1,4 @@ -// -build windows +//go:build windows // SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 diff --git a/vendor/github.com/go-openapi/spec/swagger.go b/vendor/github.com/go-openapi/spec/swagger.go index dbe32db..61a278e 100644 --- a/vendor/github.com/go-openapi/spec/swagger.go +++ b/vendor/github.com/go-openapi/spec/swagger.go @@ -403,7 +403,9 @@ func (s *SchemaOrArray) ContainsType(name string) bool { // MarshalJSON converts this schema object or array into JSON structure. func (s SchemaOrArray) MarshalJSON() ([]byte, error) { - if len(s.Schemas) > 0 { + if s.Schemas != nil { + // an empty array is still the array form: marshalling it as the single form + // would yield "null", which is not a schema return json.Marshal(s.Schemas) } return json.Marshal(s.Schema) diff --git a/vendor/github.com/go-openapi/swag/fileutils/doc.go b/vendor/github.com/go-openapi/swag/fileutils/doc.go index 859a200..d5ca57b 100644 --- a/vendor/github.com/go-openapi/swag/fileutils/doc.go +++ b/vendor/github.com/go-openapi/swag/fileutils/doc.go @@ -3,8 +3,14 @@ // Package fileutils exposes utilities to deal with files and paths. // -// Currently, there is: -// - [File] to represent an abstraction of an uploaded file. -// For instance, this is used by [github.com/go-openapi/runtime.File]. -// - path search utilities (e.g. finding packages in the GO search path) +// It provides: +// +// - [File], an abstraction of an uploaded file. +// It is used by [github.com/go-openapi/runtime.File]. +// - Implementations of [fs.FS]: [OsFS] and [GlobOsFS] wrap the os package, +// [MapFS] serves files held in memory, [OverlayFS] stacks file systems on top of one another, +// [OpaqueFS] lets a layer claim a directory for itself, and [FileReaderFS] adds a ReadFile +// method to any [fs.FS]. +// - [MustSub], to re-root a file system inline when the directory is a constant of the program. +// - path search utilities, to locate a package in the go search path. package fileutils diff --git a/vendor/github.com/go-openapi/swag/fileutils/file.go b/vendor/github.com/go-openapi/swag/fileutils/file.go index 5ad4cfa..a5ff03c 100644 --- a/vendor/github.com/go-openapi/swag/fileutils/file.go +++ b/vendor/github.com/go-openapi/swag/fileutils/file.go @@ -6,17 +6,22 @@ package fileutils import "mime/multipart" // File represents an uploaded file. +// +// Data holds the payload, and Header the multipart metadata. +// File implements [io.ReadCloser] by delegating both methods to Data. +// +// The zero File is not usable: [File.Read] and [File.Close] both panic when Data is nil. type File struct { Data multipart.File Header *multipart.FileHeader } -// Read bytes from the file +// Read reads bytes from the payload. func (f *File) Read(p []byte) (n int, err error) { return f.Data.Read(p) } -// Close the file +// Close closes the payload. func (f *File) Close() error { return f.Data.Close() } diff --git a/vendor/github.com/go-openapi/swag/fileutils/fs.go b/vendor/github.com/go-openapi/swag/fileutils/fs.go new file mode 100644 index 0000000..56b0a1f --- /dev/null +++ b/vendor/github.com/go-openapi/swag/fileutils/fs.go @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package fileutils + +import ( + "fmt" + "io/fs" + "os" +) + +// OsFS exposes package os features as an [fs.FS], without having to use [os.Root]. +// +// Existing alternatives from the standard library are [os.DirFS], which requires a base directory, +// and [os.Root.FS], which requires a root. +// [OsFS] is intended to be used when none of these alternatives are workable, +// that is when the caller does not know which root it should run in. +// +// Names are passed to the os package unchanged, +// so [OsFS] accepts absolute and relative paths, which a conforming [fs.FS] rejects. +// It offers no containment: every file that the process may read is reachable. +// +// [OsFS] implements [fs.FS], [fs.ReadFileFS] and [fs.ReadDirFS]. +type OsFS struct { +} + +// NewReadOnlyOsFS builds an [OsFS], a read-only view of the os file system. +func NewReadOnlyOsFS() *OsFS { + return &OsFS{} +} + +// Open opens the named file for reading. +func (f *OsFS) Open(name string) (fs.File, error) { + return os.Open(name) +} + +// ReadFile reads the named file and returns its content. +func (f *OsFS) ReadFile(name string) ([]byte, error) { + return os.ReadFile(name) +} + +// ReadDir reads the named directory and returns its entries sorted by file name. +func (f *OsFS) ReadDir(name string) ([]fs.DirEntry, error) { + return os.ReadDir(name) +} + +// FileReaderFS makes a [fs.FS] into a [fs.ReadFileFS], with a [FileReaderFS.ReadFile] method. +type FileReaderFS struct { + fs.FS +} + +// NewFileReaderFS transforms a [fs.FS] into a [fs.ReadFileFS]. +func NewFileReaderFS(base fs.FS) *FileReaderFS { + return &FileReaderFS{ + FS: base, + } +} + +// ReadFile reads the named file from the base file system and returns its content. +func (f *FileReaderFS) ReadFile(name string) ([]byte, error) { + return fs.ReadFile(f.FS, name) +} + +// GlobOsFS is an [OsFS] that also implements [fs.GlobFS], with a [GlobOsFS.Glob] method. +type GlobOsFS struct { + *OsFS +} + +// NewGlobOsFS is like [NewReadOnlyOsFS], augmented to match the [fs.GlobFS] interface. +func NewGlobOsFS() *GlobOsFS { + return &GlobOsFS{ + OsFS: NewReadOnlyOsFS(), + } +} + +// Glob returns the names matching pattern, sorted in lexical order. +// +// It returns a nil slice and no error when nothing matches, +// and [path.ErrBadPattern] when the pattern is malformed. +func (f *GlobOsFS) Glob(pattern string) ([]string, error) { + return fs.Glob(f.OsFS, pattern) +} + +// MustSub re-roots a file system at one of its directories, and panics when it cannot. +// +// It is [fs.Sub] for the cases where the directory is a constant of the program, such as a folder +// of an [embed.FS] assembled at initialization time: there, a failure means the program is wrong, +// not that its input is. +// +// Use [fs.Sub] itself whenever the directory comes from the outside, such as a flag, +// a configuration file or a request, so that an invalid one is reported rather than fatal. +// +// It is meant to be composed inline: +// +// assets := NewOverlayFS( +// MustSub(embedded, "templates"), +// MustSub(embedded, "templates/contrib/mine"), +// ) +func MustSub(fsys fs.FS, dir string) fs.FS { + subFS, err := fs.Sub(fsys, dir) + if err != nil { + panic(fmt.Errorf("fileutils.MustSub: cannot re-root at %q: %w", dir, err)) + } + + return subFS +} diff --git a/vendor/github.com/go-openapi/swag/fileutils/mapfs.go b/vendor/github.com/go-openapi/swag/fileutils/mapfs.go new file mode 100644 index 0000000..5a81879 --- /dev/null +++ b/vendor/github.com/go-openapi/swag/fileutils/mapfs.go @@ -0,0 +1,366 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package fileutils + +import ( + "bytes" + "errors" + "io" + "io/fs" + "path" + "slices" + "strings" + "time" +) + +// Default modes of the entries of a [MapFS]. +// +// A [MapFS] is read-only, so its files and directories are readable and never writable. +const ( + // DefaultFileMode is the mode reported by a file with no [MapFile.Mode] of its own. + DefaultFileMode fs.FileMode = 0o444 + + // DefaultDirMode is the mode reported by the directories of a [MapFS]. + DefaultDirMode fs.FileMode = fs.ModeDir | 0o555 +) + +var ( + // errNameIsFileAndDir is reported when the same name is held as a file and as a parent directory. + errNameIsFileAndDir = errors.New("name is held both as a file and as a directory") + + // errNotDir is reported when a regular file is listed as a directory. + errNotDir = errors.New("not a directory") +) + +// MapFile is a file held by a [MapFS]. +// +// Only [MapFile.Data] is required. [NewMapFS] fills the remaining fields with presets when they +// are left to their zero value. +type MapFile struct { + // Data is the content of the file. + Data []byte + + // Mode is the file mode reported by [fs.FileInfo.Mode]. Zero means [DefaultFileMode]. + Mode fs.FileMode + + // ModTime is the modification time reported by [fs.FileInfo.ModTime]. + // + // The zero value is left as is, so that a [MapFS] built from the same input twice + // reports the same metadata. + ModTime time.Time + + // Sys is the opaque value reported by [fs.FileInfo.Sys]. + Sys any +} + +// MapFS is a read-only in-memory [fs.FS], built from a map of file names to content. +// +// It is intended for the cases where the files to serve are held in memory rather than on disk: +// an overlay assembled from raw bytes, assets that a configuration provides, or a fixture in a test. +// +// Names are slash-separated paths, as accepted by [fs.ValidPath]. [NewMapFS] cleans them, +// so a name may be given with a leading "./" or "/", or with redundant elements. +// A name that remains invalid once cleaned, such as one climbing above the root, is reported +// as an error rather than dropped. +// +// Separators are never translated, so that the same input yields the same file system on every +// platform: a caller holding os paths converts them with [path/filepath.ToSlash] beforehand. +// +// Directories are implied by the names of the files, and are indexed once, when the file system +// is built: a name holds a file, and every one of its parents holds a directory. +// The root "." always exists, even when the file system holds no file at all. +// +// [MapFS] implements [fs.FS], [fs.ReadFileFS], [fs.StatFS] and [fs.ReadDirFS], but not [fs.GlobFS]: +// [fs.Glob] resolves against it all the same, through [MapFS.ReadDir]. +type MapFS struct { + files map[string]MapFile + dirs map[string][]fs.DirEntry +} + +// NewMapFS builds an in-memory file system from a map of file names to content. +// +// Names are normalized, and reported as an error when they remain invalid, or when the same name +// is held both as a file and as the parent directory of another one. +// A [MapFile] left with a zero [MapFile.Mode] reports [DefaultFileMode]. +// +// The map is copied, so adding or removing an entry afterwards leaves the file system alone. +// The contents are not: a caller that writes to a [MapFile.Data] slice it still holds changes +// what the file system serves. Hand over a slice nothing else keeps, or copy it first. +func NewMapFS(files map[string]MapFile) (*MapFS, error) { + normalized := make(map[string]MapFile, len(files)) + + for name, file := range files { + clean, err := normalizeMapName(name) + if err != nil { + return nil, err + } + + if _, isDuplicate := normalized[clean]; isDuplicate { + return nil, &fs.PathError{Op: "newmapfs", Path: clean, Err: fs.ErrExist} + } + + if file.Mode == 0 { + file.Mode = DefaultFileMode + } + + normalized[clean] = file + } + + dirs, err := indexMapDirs(normalized) + if err != nil { + return nil, err + } + + return &MapFS{ + files: normalized, + dirs: dirs, + }, nil +} + +// FromRawMap builds the files of a [MapFS] from raw contents, leaving every metadata field to its preset. +// +// It is the shortest way to a [MapFS] when all the caller holds is bytes: +// +// mapFS, err := NewMapFS(FromRawMap(map[string][]byte{ +// "folder/file1": raw1, +// "folder/file2": raw2, +// })) +func FromRawMap(raw map[string][]byte) map[string]MapFile { + files := make(map[string]MapFile, len(raw)) + for name, data := range raw { + files[name] = MapFile{Data: data} + } + + return files +} + +// Open opens the named file or directory. +// +// Opening a directory yields a [fs.ReadDirFile] reporting the same entries as [MapFS.ReadDir]. +func (f *MapFS) Open(name string) (fs.File, error) { + if !fs.ValidPath(name) { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid} + } + + if file, isFile := f.files[name]; isFile { + return &openMapFile{ + info: mapFileInfo{name: path.Base(name), file: file}, + reader: bytes.NewReader(file.Data), + }, nil + } + + entries, isDir := f.dirs[name] + if !isDir { + return nil, notFound("open", name) + } + + return &openMapDir{info: mapDirInfo{name: path.Base(name)}, entries: entries}, nil +} + +// ReadFile reads the named file and returns a copy of its content. +func (f *MapFS) ReadFile(name string) ([]byte, error) { + if !fs.ValidPath(name) { + return nil, &fs.PathError{Op: "read", Path: name, Err: fs.ErrInvalid} + } + + file, isFile := f.files[name] + if !isFile { + if _, isDir := f.dirs[name]; isDir { + return nil, &fs.PathError{Op: "read", Path: name, Err: errIsDir} + } + + return nil, notFound("read", name) + } + + // a caller mutating the result would otherwise mutate what the file system serves + return slices.Clone(file.Data), nil +} + +// Stat returns the [fs.FileInfo] of the named file or directory. +func (f *MapFS) Stat(name string) (fs.FileInfo, error) { + if !fs.ValidPath(name) { + return nil, &fs.PathError{Op: "stat", Path: name, Err: fs.ErrInvalid} + } + + if file, isFile := f.files[name]; isFile { + return mapFileInfo{name: path.Base(name), file: file}, nil + } + + if _, isDir := f.dirs[name]; isDir { + return mapDirInfo{name: path.Base(name)}, nil + } + + return nil, notFound("stat", name) +} + +// ReadDir lists the named directory, with its entries sorted by file name. +func (f *MapFS) ReadDir(name string) ([]fs.DirEntry, error) { + if !fs.ValidPath(name) { + return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrInvalid} + } + + entries, isDir := f.dirs[name] + if !isDir { + if _, isFile := f.files[name]; isFile { + return nil, &fs.PathError{Op: "readdir", Path: name, Err: errNotDir} + } + + return nil, notFound("readdir", name) + } + + // the index is shared by every caller, so it must not escape + return slices.Clone(entries), nil +} + +// normalizeMapName turns the name of a file into the cleaned form that [fs.ValidPath] accepts. +// +// Separators are left alone: an [fs.FS] name is slash-separated by definition, and translating +// them here would resolve the same input differently depending on the platform. +func normalizeMapName(name string) (string, error) { + clean := path.Clean(strings.TrimPrefix(name, "/")) + + if !fs.ValidPath(clean) || clean == "." { + return "", &fs.PathError{Op: "newmapfs", Path: name, Err: fs.ErrInvalid} + } + + return clean, nil +} + +// indexMapDirs builds the directory index of a [MapFS], once, from the names of its files. +// +// Every parent of a file name holds a directory, up to the root, which always exists. +func indexMapDirs(files map[string]MapFile) (map[string][]fs.DirEntry, error) { + children := map[string]map[string]bool{".": {}} // directory -> child base name -> is a directory + + for name := range files { + dir := path.Dir(name) + addMapChild(children, dir, path.Base(name), false) + + // every ancestor of the file holds a directory + for dir != "." { + parent := path.Dir(dir) + addMapChild(children, parent, path.Base(dir), true) + dir = parent + } + } + + dirs := make(map[string][]fs.DirEntry, len(children)) + for dir, names := range children { + if _, isFile := files[dir]; isFile { + return nil, &fs.PathError{Op: "newmapfs", Path: dir, Err: errNameIsFileAndDir} + } + + entries := make([]fs.DirEntry, 0, len(names)) + for base, isDir := range names { + if isDir { + entries = append(entries, mapDirInfo{name: base}) + + continue + } + + entries = append(entries, mapFileInfo{name: base, file: files[path.Join(dir, base)]}) + } + + slices.SortFunc(entries, func(a, b fs.DirEntry) int { + return strings.Compare(a.Name(), b.Name()) + }) + dirs[dir] = entries + } + + return dirs, nil +} + +// addMapChild records that a directory holds an entry. +func addMapChild(children map[string]map[string]bool, dir, base string, isDir bool) { + entries, exists := children[dir] + if !exists { + entries = make(map[string]bool) + children[dir] = entries + } + + entries[base] = isDir +} + +// mapFileInfo reports the metadata of a file of a [MapFS]. It is both a [fs.FileInfo] and a [fs.DirEntry]. +type mapFileInfo struct { + name string + file MapFile +} + +func (i mapFileInfo) Name() string { return i.name } +func (i mapFileInfo) Size() int64 { return int64(len(i.file.Data)) } +func (i mapFileInfo) Mode() fs.FileMode { return i.file.Mode } +func (i mapFileInfo) Type() fs.FileMode { return i.file.Mode.Type() } +func (i mapFileInfo) ModTime() time.Time { return i.file.ModTime } +func (i mapFileInfo) IsDir() bool { return false } +func (i mapFileInfo) Sys() any { return i.file.Sys } +func (i mapFileInfo) Info() (fs.FileInfo, error) { return i, nil } + +// mapDirInfo reports the metadata of a directory of a [MapFS]. It is both a [fs.FileInfo] and a [fs.DirEntry]. +// +// Directories are implied by the names of the files, so they carry no metadata of their own. +type mapDirInfo struct { + name string +} + +func (i mapDirInfo) Name() string { return i.name } +func (i mapDirInfo) Size() int64 { return 0 } +func (i mapDirInfo) Mode() fs.FileMode { return DefaultDirMode } +func (i mapDirInfo) Type() fs.FileMode { return fs.ModeDir } +func (i mapDirInfo) ModTime() time.Time { return time.Time{} } +func (i mapDirInfo) IsDir() bool { return true } +func (i mapDirInfo) Sys() any { return nil } +func (i mapDirInfo) Info() (fs.FileInfo, error) { return i, nil } + +// openMapFile is the [fs.File] returned when opening a file of a [MapFS]. +type openMapFile struct { + info mapFileInfo + reader *bytes.Reader +} + +func (f *openMapFile) Stat() (fs.FileInfo, error) { return f.info, nil } + +func (f *openMapFile) Close() error { return nil } + +func (f *openMapFile) Read(p []byte) (int, error) { return f.reader.Read(p) } + +func (f *openMapFile) Seek(offset int64, whence int) (int64, error) { + return f.reader.Seek(offset, whence) +} + +// openMapDir is the [fs.ReadDirFile] returned when opening a directory of a [MapFS]. +type openMapDir struct { + info mapDirInfo + entries []fs.DirEntry + offset int +} + +func (d *openMapDir) Stat() (fs.FileInfo, error) { return d.info, nil } + +func (d *openMapDir) Close() error { return nil } + +func (d *openMapDir) Read([]byte) (int, error) { + return 0, &fs.PathError{Op: "read", Path: d.info.name, Err: errIsDir} +} + +// ReadDir returns the next n entries, or all the remaining ones when n is not positive. +func (d *openMapDir) ReadDir(n int) ([]fs.DirEntry, error) { + remaining := len(d.entries) - d.offset + if n <= 0 { + entries := slices.Clone(d.entries[d.offset:]) + d.offset = len(d.entries) + + return entries, nil + } + + if remaining == 0 { + return nil, io.EOF + } + + n = min(n, remaining) + entries := slices.Clone(d.entries[d.offset : d.offset+n]) + d.offset += n + + return entries, nil +} diff --git a/vendor/github.com/go-openapi/swag/fileutils/opaque.go b/vendor/github.com/go-openapi/swag/fileutils/opaque.go new file mode 100644 index 0000000..526e867 --- /dev/null +++ b/vendor/github.com/go-openapi/swag/fileutils/opaque.go @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package fileutils + +import ( + "io/fs" + "path" +) + +// OpaqueDirFS is a file system that entirely owns some of its directories. +// +// [OverlayFS] neither merges an opaque directory with the layers below it, +// nor resolves any name under it against them: +// what the owning layer holds replaces what they hold under the same name. +// +// [OpaqueFS] is the implementation provided by this package. +type OpaqueDirFS interface { + fs.FS + + // IsOpaqueDir reports whether a directory was declared as entirely owned by this file system. + // + // It answers from the declarations given to [NewOpaqueFS], and does not check that the file + // system holds the directory: a name declared opaque reports true either way. + // [OverlayFS] consults it only for a layer that holds the directory. + IsOpaqueDir(name string) bool +} + +// OpaqueDirsAll declares that a file system owns every directory it holds, except its root. +// +// It is the only pattern recognized by [NewOpaqueFS]: directories are otherwise matched +// by their exact name. +const OpaqueDirsAll = "*" + +// OpaqueFS makes a [fs.FS] into an [OpaqueDirFS], by declaring the directories that it owns. +type OpaqueFS struct { + fs.FS + + opaqueDirs map[string]struct{} +} + +// NewOpaqueFS declares the directories that a file system entirely owns, +// so that they shadow the layers below when it is stacked in an [OverlayFS]. +// +// Directories are slash-separated paths, as accepted by [fs.ValidPath], and are cleaned. +// Declaring "." makes the whole file system opaque, down from its root. +// Declaring [OpaqueDirsAll] makes every directory opaque but the root, +// so that the root still merges and the layers below keep contributing what they hold beside it. +// +// Declaring a directory that this file system does not hold changes nothing once it is stacked. +// [OverlayFS] skips a layer that does not hold the directory before it consults opacity, +// so the layers below keep resolving that directory and everything under it. +func NewOpaqueFS(base fs.FS, dirs ...string) *OpaqueFS { + opaqueDirs := make(map[string]struct{}, len(dirs)) + for _, dir := range dirs { + opaqueDirs[path.Clean(dir)] = struct{}{} + } + + return &OpaqueFS{ + FS: base, + opaqueDirs: opaqueDirs, + } +} + +// IsOpaqueDir reports whether a directory was declared as entirely owned by this file system. +// +// It answers from the declarations given to [NewOpaqueFS], and does not check that the file +// system holds the directory: a name declared opaque reports true either way. +// [OverlayFS] consults it only for a layer that holds the directory. +func (f *OpaqueFS) IsOpaqueDir(name string) bool { + if _, isOpaque := f.opaqueDirs[name]; isOpaque { + return true + } + + if name == "." { + // the root is owned only when it is declared explicitly + return false + } + + _, ownsAll := f.opaqueDirs[OpaqueDirsAll] + + return ownsAll +} + +// ReadFile reads a file from the wrapped file system. +func (f *OpaqueFS) ReadFile(name string) ([]byte, error) { + return fs.ReadFile(f.FS, name) +} + +// Stat returns the [fs.FileInfo] of a name in the wrapped file system. +func (f *OpaqueFS) Stat(name string) (fs.FileInfo, error) { + return fs.Stat(f.FS, name) +} + +// ReadDir lists a directory of the wrapped file system. +func (f *OpaqueFS) ReadDir(name string) ([]fs.DirEntry, error) { + return fs.ReadDir(f.FS, name) +} diff --git a/vendor/github.com/go-openapi/swag/fileutils/overlay.go b/vendor/github.com/go-openapi/swag/fileutils/overlay.go new file mode 100644 index 0000000..d67ab6a --- /dev/null +++ b/vendor/github.com/go-openapi/swag/fileutils/overlay.go @@ -0,0 +1,345 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package fileutils + +import ( + "errors" + "io" + "io/fs" + "path" + "slices" + "strings" + "syscall" +) + +// OverlayFS is a read-only [fs.FS] that stacks overlays on top of a base file system. +// +// A name is resolved in the topmost layer that holds it, then down to the base. +// See [NewOverlayFS] for the order in which layers are stacked. +// When the name is absent from all layers, every method returns a [fs.PathError] that reports +// the name and matches [fs.ErrNotExist]. +// +// [OverlayFS] implements [fs.FS], [fs.ReadFileFS], [fs.StatFS] and [fs.ReadDirFS], but not [fs.GlobFS]. +// A directory returns an error when the resolved layer does not support reading directories. +// +// Directories are merged: a directory reports the union of the entries held by every layer, +// and the topmost layer wins whenever the same name is held by several of them. +// A layer may claim a directory for itself with [NewOpaqueFS], which stops the merge +// and hides everything the lower layers hold under that directory. +type OverlayFS struct { + layers []fs.FS +} + +// NewOverlayFS builds an overlay file system from a base file system and a list of overlays. +// +// Overlays are stacked in the order in which they are provided: +// the last one sits on top and is resolved first, then the preceding ones in reverse order. +// The base is always resolved last. +// +// An empty list of overlays yields a file system that resolves against the base alone. +func NewOverlayFS(base fs.FS, overlays ...fs.FS) *OverlayFS { + layers := make([]fs.FS, 0, len(overlays)+1) + for _, overlay := range slices.Backward(overlays) { + layers = append(layers, overlay) + } + layers = append(layers, base) + + return &OverlayFS{ + layers: layers, + } +} + +// Open opens a file, resolving layers from the topmost overlay down to the base. +// +// Opening a directory yields a [fs.ReadDirFile] that reports the same entries as +// [OverlayFS.ReadDir], so that a merged directory reads alike either way. +func (f *OverlayFS) Open(name string) (fs.File, error) { + file, err := f.openInLayers(name) + if err != nil { + return nil, err + } + + info, err := file.Stat() + if err != nil { + _ = file.Close() + + return nil, err + } + + if !info.IsDir() { + return file, nil + } + + // the entries of a directory come from every layer, not from the one that resolved it + _ = file.Close() + + entries, err := f.readMergedDir(name) + if err != nil { + return nil, err + } + + return &mergedDir{name: name, info: info, entries: entries}, nil +} + +// ReadFile reads a file, resolving layers from the topmost overlay down to the base. +func (f *OverlayFS) ReadFile(name string) ([]byte, error) { + layer, err := f.findInLayers("open", name) + if err != nil { + return nil, err + } + + return fs.ReadFile(layer, name) +} + +// Stat returns the [fs.FileInfo] of a file, resolving layers from the topmost overlay down to the base. +func (f *OverlayFS) Stat(name string) (fs.FileInfo, error) { + layer, err := f.findInLayers("stat", name) + if err != nil { + return nil, err + } + + return fs.Stat(layer, name) +} + +// ReadDir lists a directory, resolving layers from the topmost overlay down to the base. +// +// The entries of every layer holding the directory are merged, sorted by file name, +// and a name held by several layers is reported by the topmost of them. +// The merge stops at the topmost layer that owns the directory, as declared by [NewOpaqueFS]. +func (f *OverlayFS) ReadDir(name string) ([]fs.DirEntry, error) { + return f.readMergedDir(name) +} + +// readMergedDir collects the entries of every layer that holds a directory. +// +// A name that is not a directory in some layer shadows the layers below it, +// just like a regular file does. +func (f *OverlayFS) readMergedDir(name string) ([]fs.DirEntry, error) { + var ( + merged []fs.DirEntry + found bool + ) + seen := make(map[string]struct{}) + + for _, layer := range f.layers { + info, err := fs.Stat(layer, name) + if err != nil { + if !isNotFound(err) { + return nil, err + } + + if ownsSubtreeOf(layer, name) { + break + } + + continue + } + + if !info.IsDir() { + if found { + // a directory held by an upper layer shadows this entry + break + } + + // let the layer report why the name cannot be listed + return fs.ReadDir(layer, name) + } + + entries, err := fs.ReadDir(layer, name) + if err != nil { + return nil, err + } + + found = true + for _, entry := range entries { + if _, isShadowed := seen[entry.Name()]; isShadowed { + continue + } + + seen[entry.Name()] = struct{}{} + merged = append(merged, entry) + } + + if declaresOpaqueDir(layer, name) || ownsSubtreeOf(layer, name) { + // this layer owns the directory, or one of its parents: + // the layers below it contribute nothing + break + } + } + + if !found { + return nil, notFound("readdir", name) + } + + slices.SortFunc(merged, func(a, b fs.DirEntry) int { + return strings.Compare(a.Name(), b.Name()) + }) + + return merged, nil +} + +// openInLayers opens a name in the topmost layer that holds it. +func (f *OverlayFS) openInLayers(name string) (fs.File, error) { + for _, layer := range f.layers { + file, err := layer.Open(name) + if err == nil { + return file, nil + } + + if !isNotFound(err) { + return nil, err + } + + if ownsSubtreeOf(layer, name) { + break + } + } + + return nil, notFound("open", name) +} + +// findInLayers returns the topmost layer that holds a name. +// +// op names the operation reported by the [fs.PathError] raised when no layer holds the name. +func (f *OverlayFS) findInLayers(op, name string) (fs.FS, error) { + for _, layer := range f.layers { + err := probeInLayer(layer, name) + if err == nil { + return layer, nil + } + + if !isNotFound(err) { + return nil, err + } + + if ownsSubtreeOf(layer, name) { + break + } + } + + return nil, notFound(op, name) +} + +// probeInLayer reports the error raised by a layer when looking a name up. +// +// A layer implementing [fs.StatFS] is probed with Stat. +// Otherwise the name is opened, then closed again. +func probeInLayer(layer fs.FS, name string) error { + if statFS, supportsStat := layer.(fs.StatFS); supportsStat { + _, err := statFS.Stat(name) + + return err + } + + file, err := layer.Open(name) + if err == nil { + _ = file.Close() + } + + return err +} + +// declaresOpaqueDir tells whether a layer marks a directory as entirely owned. +func declaresOpaqueDir(layer fs.FS, name string) bool { + opaque, isOpaqueFS := layer.(OpaqueDirFS) + + return isOpaqueFS && opaque.IsOpaqueDir(name) +} + +// ownsDir tells whether a layer marks a directory as entirely owned, and holds it. +func ownsDir(layer fs.FS, name string) bool { + if !declaresOpaqueDir(layer, name) { + return false + } + + info, err := fs.Stat(layer, name) + + return err == nil && info.IsDir() +} + +// ownsSubtreeOf tells whether a layer owns one of the parent directories of a name. +// +// The layers below such a layer hold nothing that is reachable under that name. +func ownsSubtreeOf(layer fs.FS, name string) bool { + if _, isOpaqueFS := layer.(OpaqueDirFS); !isOpaqueFS { + return false + } + + for dir := path.Dir(name); ; { + if ownsDir(layer, dir) { + return true + } + + // "." and "/" are their own parent, so this is where the walk up ends + parent := path.Dir(dir) + if parent == dir { + return false + } + + dir = parent + } +} + +// errIsDir is reported when a merged directory is read as a regular file. +var errIsDir = errors.New("is a directory") + +// mergedDir is the [fs.ReadDirFile] returned when opening a directory with merged layers. +// +// Its [fs.FileInfo] is the one of the topmost layer holding the directory, +// while its entries come from all the layers holding it. +type mergedDir struct { + name string + info fs.FileInfo + entries []fs.DirEntry + offset int +} + +func (d *mergedDir) Stat() (fs.FileInfo, error) { return d.info, nil } + +func (d *mergedDir) Close() error { return nil } + +func (d *mergedDir) Read([]byte) (int, error) { + return 0, &fs.PathError{Op: "read", Path: d.name, Err: errIsDir} +} + +// ReadDir returns the next n entries, or all the remaining ones when n is not positive. +func (d *mergedDir) ReadDir(n int) ([]fs.DirEntry, error) { + remaining := len(d.entries) - d.offset + if n <= 0 { + entries := d.entries[d.offset:] + d.offset = len(d.entries) + + return entries, nil + } + + if remaining == 0 { + return nil, io.EOF + } + + n = min(n, remaining) + entries := d.entries[d.offset : d.offset+n] + d.offset += n + + return entries, nil +} + +// notFound builds the error reported when a name is absent from all layers. +// +// It mirrors what the os package raises for a missing file, +// so that a caller may retrieve the name from the error alone. +func notFound(op, name string) error { + return &fs.PathError{Op: op, Path: name, Err: fs.ErrNotExist} +} + +// isNotFound tells whether an error raised by a layer means that the name is absent from that layer. +// +// The whole error chain is inspected, so a layer that wraps its errors resolves like any other, +// and so does a layer that is itself an [OverlayFS]. +// +// [syscall.ENOTDIR] is matched explicitly, because a path traversing a regular file +// does not report [fs.ErrNotExist] on all platforms. +// Any other error stops the resolution and is reported to the caller. +func isNotFound(err error) bool { + return errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) +} diff --git a/vendor/github.com/go-openapi/swag/fileutils/path.go b/vendor/github.com/go-openapi/swag/fileutils/path.go index dd09f69..647df76 100644 --- a/vendor/github.com/go-openapi/swag/fileutils/path.go +++ b/vendor/github.com/go-openapi/swag/fileutils/path.go @@ -10,10 +10,17 @@ import ( "strings" ) -// GOPATHKey represents the env key for gopath +// GOPATHKey is the name of the environment variable that holds the go search path. const GOPATHKey = "GOPATH" -// FindInSearchPath finds a package in a provided lists of paths +// FindInSearchPath finds a package in a list of search paths. +// +// searchPath lists directories separated by the OS path separator, +// in the form accepted by [filepath.SplitList]. +// Each directory is probed for a src/pkg subdirectory. +// +// It returns the first match, with symlinks resolved, +// or an empty string when the package is not found in any of the directories. func FindInSearchPath(searchPath, pkg string) string { pathsList := filepath.SplitList(searchPath) for _, path := range pathsList { @@ -26,7 +33,9 @@ func FindInSearchPath(searchPath, pkg string) string { return "" } -// FindInGoSearchPath finds a package in the $GOPATH:$GOROOT +// FindInGoSearchPath finds a package in $GOPATH and $GOROOT. +// +// It returns an empty string when the package is not found. // // Deprecated: this function is no longer relevant with modern go. // It uses [runtime.GOROOT] under the hood, which is deprecated as of go1.24. @@ -34,7 +43,10 @@ func FindInGoSearchPath(pkg string) string { return FindInSearchPath(FullGoSearchPath(), pkg) } -// FullGoSearchPath gets the search paths for finding packages +// FullGoSearchPath returns the search paths in which a package may be found. +// +// It joins $GOPATH, which defaults to $HOME/go when unset, with [runtime.GOROOT]. +// The two are separated by a colon, so the result is not usable on windows. // // Deprecated: this function is no longer relevant with modern go. // It uses [runtime.GOROOT] under the hood, which is deprecated as of go1.24. diff --git a/vendor/github.com/go-openapi/validate/.golangci.yml b/vendor/github.com/go-openapi/validate/.golangci.yml index b8875d7..c05f38e 100644 --- a/vendor/github.com/go-openapi/validate/.golangci.yml +++ b/vendor/github.com/go-openapi/validate/.golangci.yml @@ -10,6 +10,7 @@ linters: - gomodguard - gomodguard_v2 - exhaustruct + - exhaustruct_v5 - ireturn - nlreturn - nestif diff --git a/vendor/github.com/go-openapi/validate/BENCHMARK.md b/vendor/github.com/go-openapi/validate/BENCHMARK.md deleted file mode 100644 index 0353eae..0000000 --- a/vendor/github.com/go-openapi/validate/BENCHMARK.md +++ /dev/null @@ -1,34 +0,0 @@ -# Benchmark - -Validating the Kubernetes Swagger API - -## v0.22.6: 60,000,000 allocs - -``` -goos: linux -goarch: amd64 -pkg: github.com/go-openapi/validate -cpu: AMD Ryzen 7 5800X 8-Core Processor -Benchmark_KubernetesSpec/validating_kubernetes_API-16 1 8549863982 ns/op 7067424936 B/op 59583275 allocs/op -``` - -## After refact PR: minor but noticeable improvements: 25,000,000 allocs - -``` -go test -bench Spec -goos: linux -goarch: amd64 -pkg: github.com/go-openapi/validate -cpu: AMD Ryzen 7 5800X 8-Core Processor -Benchmark_KubernetesSpec/validating_kubernetes_API-16 1 4064535557 ns/op 3379715592 B/op 25320330 allocs/op -``` - -## After reduce GC pressure PR: 17,000,000 allocs - -``` -goos: linux -goarch: amd64 -pkg: github.com/go-openapi/validate -cpu: AMD Ryzen 7 5800X 8-Core Processor -Benchmark_KubernetesSpec/validating_kubernetes_API-16 1 3758414145 ns/op 2593881496 B/op 17111373 allocs/op -``` diff --git a/vendor/github.com/go-openapi/validate/README.md b/vendor/github.com/go-openapi/validate/README.md index b814a2e..aa8eea5 100644 --- a/vendor/github.com/go-openapi/validate/README.md +++ b/vendor/github.com/go-openapi/validate/README.md @@ -5,7 +5,7 @@ -[![Release][release-badge]][release-url] [![Go Report Card][gocard-badge]][gocard-url] [![CodeFactor Grade][codefactor-badge]][codefactor-url] [![License][license-badge]][license-url] +[![Release][release-badge]][release-url] [![CodeFactor Grade][codefactor-badge]][codefactor-url] [![License][license-badge]][license-url] [![GoDoc][godoc-badge]][godoc-url] [![Discord Channel][discord-badge]][discord-url] [![go version][goversion-badge]][goversion-url] ![Top language][top-badge] ![Commits since latest release][commits-badge] @@ -98,8 +98,6 @@ Maintainers can cut a new release by either: [release-badge]: https://badge.fury.io/gh/go-openapi%2Fvalidate.svg [release-url]: https://badge.fury.io/gh/go-openapi%2Fvalidate -[gocard-badge]: https://goreportcard.com/badge/github.com/go-openapi/validate -[gocard-url]: https://goreportcard.com/report/github.com/go-openapi/validate [codefactor-badge]: https://img.shields.io/codefactor/grade/github/go-openapi/validate [codefactor-url]: https://www.codefactor.io/repository/github/go-openapi/validate diff --git a/vendor/github.com/go-openapi/validate/collection_format.go b/vendor/github.com/go-openapi/validate/collection_format.go new file mode 100644 index 0000000..a77ba56 --- /dev/null +++ b/vendor/github.com/go-openapi/validate/collection_format.go @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package validate + +import ( + "fmt" + "strconv" + + "github.com/go-openapi/spec" +) + +// validateCollectionFormats warns about a collectionFormat that has no effect. +// +// collectionFormat says how to join the members of an array into one value on the wire — csv, ssv, +// tsv, pipes, or multi for a parameter repeated once per member: +// +// parameters: +// - name: tags +// in: query +// type: array +// items: { type: string } +// collectionFormat: pipes +// +// There is nothing to join when the type is not array, so a collectionFormat written on a string or +// an integer does nothing. Swagger 2.0 says the member "determines the format of the array if type +// array is used" and stops there — it never forbids writing it elsewhere, so this is a warning and +// the specification stays valid. +// +// The meta-schema already covers the rest of the collectionFormat rules, and covers them at every +// location: the value must be one of csv, ssv, tsv or pipes, widened with multi for a query or +// formData parameter, where repeating the parameter is possible. A body parameter cannot carry the +// member at all. None of that needs a rule here. +// +// Only a parameter, a header and an items carry a collectionFormat — [spec.SimpleSchema] holds it, +// and [spec.Schema] has no such member. So this walks what the operations of the document declare, +// the way [SpecValidator.validateItems] does, and never looks at a schema. A JSON schema validated +// on its own is untouched by this rule. +func (s *SpecValidator) validateCollectionFormats() *Result { + res := validatorPools.results.Borrow() + + operations := s.analyzer.Operations() + for _, method := range sortedKeys(operations) { + byPath := operations[method] + for _, path := range sortedKeys(byPath) { + op := byPath[path] + + for _, param := range paramHelp.safeExpandedParamsFor(path, method, op.ID, res, s) { + if param.In == swaggerBody { + // a body parameter describes itself with a schema, and the meta-schema + // forbids it a collectionFormat outright + continue + } + + at := s.parameterPath(path, method, param.In, param.Name) + in := fmt.Sprintf("parameter %q", param.Name) + checkCollectionFormat(at, param.CollectionFormat, param.Type, in, res) + checkItemsCollectionFormats(at, param.Items, in, res) + } + + for _, response := range responsesOf(op) { + at := responsePath(path, method, response.code) + for _, name := range sortedKeys(response.resp.Headers) { + header := response.resp.Headers[name] + headerAt := at.children(swaggerHeaders, name) + in := fmt.Sprintf("header %q", name) + checkCollectionFormat(headerAt, header.CollectionFormat, header.Type, in, res) + checkItemsCollectionFormats(headerAt, header.Items, in, res) + } + } + } + } + + return res +} + +// codedResponse is a response together with the code the operation files it under, "default" +// included. +type codedResponse struct { + code string + resp spec.Response +} + +// responsesOf lists the responses an operation declares, in a settled order: the default response +// first, then the status codes in ascending order. +func responsesOf(op *spec.Operation) []codedResponse { + if op == nil || op.Responses == nil { + return nil + } + + var responses []codedResponse + if op.Responses.Default != nil { + responses = append(responses, codedResponse{code: jsonDefault, resp: *op.Responses.Default}) + } + + for _, code := range sortedKeys(op.Responses.StatusCodeResponses) { + responses = append(responses, codedResponse{ + code: strconv.Itoa(code), + resp: op.Responses.StatusCodeResponses[code], + }) + } + + return responses +} + +// checkItemsCollectionFormats checks the items of a parameter or header, then the items of those +// items, as deep as the document nests them. +// +// Every level is named the same way in a message: an array of arrays that writes a pointless +// collectionFormat twice is one thing to fix, and the deeper location is reported only when the +// shallower one is sound. +func checkItemsCollectionFormats(at pathSegments, items *spec.Items, in string, res *Result) { + for items != nil { + at = at.child(jsonItems) + checkCollectionFormat(at, items.CollectionFormat, items.Type, "items of "+in, res) + items = items.Items + } +} + +// checkCollectionFormat warns when a collectionFormat is written on something that is not an array. +// +// A missing collectionFormat has nothing to answer for, and neither has a missing type: a document +// that leaves the type out is already reported by the meta-schema, and guessing what it meant here +// would only add noise. +func checkCollectionFormat(at pathSegments, collectionFormat, typ, in string, res *Result) { + if collectionFormat == "" || typ == "" || typ == arrayType { + return + } + + res.addWarningsAt(at.child(swaggerCollectionFormat), collectionFormatIgnoredMsg(collectionFormat, in, typ)) +} diff --git a/vendor/github.com/go-openapi/validate/discriminator.go b/vendor/github.com/go-openapi/validate/discriminator.go new file mode 100644 index 0000000..7a4ab41 --- /dev/null +++ b/vendor/github.com/go-openapi/validate/discriminator.go @@ -0,0 +1,156 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package validate + +import ( + "slices" + + "github.com/go-openapi/spec" +) + +// validateDiscriminators checks the discriminator of every definition, and of every schema a +// definition holds inline. +// +// A discriminator names the property that tells subtypes apart: +// +// Pet: +// discriminator: petType +// required: [ petType ] +// properties: +// petType: { type: string } +// +// Swagger 2.0 asks two things of that property: the schema must define it, and must list it as +// required. Both matter for the same reason — an instance carries its subtype in that property, +// so a subtype cannot be resolved from an instance that has nowhere to put the value, or that is +// free to leave it out. +// +// The JSON meta-schema types discriminator as a plain string and never compares it against +// properties or required, so it can express neither check. +// +// A property contributed by an allOf member counts as defined, and one that member requires counts +// as required: [SpecValidator.declaresProperty] already reads a composed definition that way for +// the required rule, and a discriminator resolves against the instance the whole composition +// describes. +// +// The third clause of the rule — the value must name this schema or one that inherits it — +// constrains the data, not the document, so it belongs to schema validation rather than here. +func (s *SpecValidator) validateDiscriminators() *Result { + res := validatorPools.results.Borrow() + definitions := s.spec.Spec().Definitions + + for _, name := range sortedKeys(definitions) { + schema := definitions[name] + s.walkDiscriminators(newPathSegments(swaggerDefinitions, name), &schema, res) + } + + return res +} + +// walkDiscriminators checks the discriminator of a schema, then of every schema it holds inline. +// +// A schema written as a $ref is left alone: it is checked where it is defined, and following it +// here would report the same fault twice and, for a recursive definition, would not terminate. +// This mirrors [SpecValidator.walkRequired]. +func (s *SpecValidator) walkDiscriminators(at pathSegments, v *spec.Schema, res *Result) { + if v == nil || v.Ref.String() != "" { + return + } + + s.checkDiscriminator(at, v, res) + + for _, name := range sortedKeys(v.Properties) { + held := v.Properties[name] + s.walkDiscriminators(at.structuralChild(jsonProperties).child(name), &held, res) + } + + for _, pattern := range sortedKeys(v.PatternProperties) { + held := v.PatternProperties[pattern] + s.walkDiscriminators(at.structuralChild(jsonPatternProperties).child(pattern), &held, res) + } + + if v.Items != nil { + if v.Items.Schema != nil { + s.walkDiscriminators(at.child(jsonItems), v.Items.Schema, res) + } + for i := range v.Items.Schemas { + s.walkDiscriminators(at.child(jsonItems).item(i), &v.Items.Schemas[i], res) + } + } + + if v.AdditionalProperties != nil && v.AdditionalProperties.Schema != nil { + s.walkDiscriminators(at.child(jsonAdditionalProperties), v.AdditionalProperties.Schema, res) + } + + for _, composition := range []struct { + keyword string + members []spec.Schema + }{ + {jsonAllOf, v.AllOf}, + {jsonAnyOf, v.AnyOf}, + {jsonOneOf, v.OneOf}, + } { + for i := range composition.members { + s.walkDiscriminators(at.child(composition.keyword).item(i), &composition.members[i], res) + } + } + + if v.Not != nil { + s.walkDiscriminators(at.child(jsonNot), v.Not, res) + } +} + +// checkDiscriminator checks the discriminator a single schema declares. A schema without one has +// nothing to answer for. +// +// Both findings are reported against the discriminator itself, which is the entry a reader has to +// go and amend, and both are reported when they apply: a discriminator naming a property that is +// neither defined nor required is two separate slips to fix. +func (s *SpecValidator) checkDiscriminator(at pathSegments, v *spec.Schema, res *Result) { + if v.Discriminator == "" { + return + } + + of := identify(at).name + discriminatorAt := at.child(jsonDiscriminator) + + if _, declared := s.declaresProperty(v, v.Discriminator, maxCompositionHops); !declared { + res.addErrorsAt(discriminatorAt, discriminatorNotDefinedMsg(v.Discriminator, of)) + } + + if !s.requiresProperty(v, v.Discriminator, maxCompositionHops) { + res.addErrorsAt(discriminatorAt, discriminatorNotRequiredMsg(v.Discriminator, of)) + } +} + +// requiresProperty reports whether a schema, or any schema composed into it by allOf, lists the +// named property as required. +// +// It is the required-list counterpart of [SpecValidator.declaresProperty], and follows allOf the +// same way, including the local $ref an allOf member may be written as. +func (s *SpecValidator) requiresProperty(v *spec.Schema, name string, hops int) bool { + if v == nil || hops <= 0 { + return false + } + + if slices.Contains(v.Required, name) { + return true + } + + for i := range v.AllOf { + member := &v.AllOf[i] + if member.Ref.String() != "" { + resolved, err := s.resolveRef(&member.Ref) + if err != nil { + continue + } + member = resolved + } + + if s.requiresProperty(member, name, hops-1) { + return true + } + } + + return false +} diff --git a/vendor/github.com/go-openapi/validate/doc.go b/vendor/github.com/go-openapi/validate/doc.go index 5218ec8..f2d02d1 100644 --- a/vendor/github.com/go-openapi/validate/doc.go +++ b/vendor/github.com/go-openapi/validate/doc.go @@ -12,6 +12,11 @@ // Validates a spec document (from JSON or YAML) against the JSON schema for swagger, // then checks a number of extra rules that can't be expressed in JSON schema. // +// The lists below hold the extra rules only. The meta-schema already settles a great deal on its +// own, and where it does, no rule is repeated here: collectionFormat, say, must be one of csv, ssv, +// tsv or pipes, widened with multi for a query or formData parameter, and a body parameter may not +// carry one at all — all of that comes out of the meta-schema, at every location it applies to. +// // Entry points: // // - Spec() @@ -25,6 +30,9 @@ // [x] path uniqueness: each api path should be non-verbatim (account for path param names) unique per method. Validation can be laxed by disabling StrictPathParamUniqueness. // [x] each security reference should contain only unique scopes // [x] each security scope in a security definition should be unique +// [x] a discriminator must name a property the schema defines and lists as required +// [x] each security requirement must name a scheme declared in securityDefinitions +// [x] only an oauth2 security requirement may list scopes: every other scheme type must list none // [x] parameters in path must be unique // [x] each path parameter must correspond to a parameter placeholder and vice versa // [x] each referenceable definition must have references @@ -47,6 +55,8 @@ // [x] unsupported validation of examples on non-JSON media types // [x] examples in response without schema // [x] readOnly properties should not be required +// [x] an oauth2 security requirement names a scope its security scheme does not declare +// [x] collectionFormat is written on a parameter, header or items whose type is not array // // # Validating a schema // @@ -69,8 +79,11 @@ // [ ] errors and warnings are not reported with key/line number in spec // [ ] default values and examples on responses only support application/json producer type // [ ] invalid numeric constraints (such as Minimum, etc..) are not checked except for default and example values -// [ ] rules for collectionFormat are not implemented -// [ ] no validation rule for polymorphism support (discriminator) [not done here] // [ ] valid js ECMA regexp not supported by Go regexp engine are considered invalid // [ ] arbitrary large numbers are not supported: max is math.MaxFloat64 +// +// Left out by design, rather than pending: a discriminator value is not checked against the schema +// names it may take. That clause of the swagger rule constrains an instance rather than the +// document, so there is nothing in a specification for [SpecValidator] to read, and checking it +// would mean resolving polymorphic payloads, which this package does not do. package validate diff --git a/vendor/github.com/go-openapi/validate/helpers.go b/vendor/github.com/go-openapi/validate/helpers.go index 62deb97..ea71436 100644 --- a/vendor/github.com/go-openapi/validate/helpers.go +++ b/vendor/github.com/go-openapi/validate/helpers.go @@ -42,6 +42,7 @@ const ( jsonRequired = "required" jsonRef = "$ref" jsonDefault = "default" + jsonDiscriminator = "discriminator" jsonAllOf = "allOf" jsonAnyOf = "anyOf" @@ -50,12 +51,17 @@ const ( jsonAdditionalItems = "additionalItems" jsonAdditionalProperties = "additionalProperties" - swaggerPaths = "paths" - swaggerDefinitions = "definitions" - swaggerResponses = "responses" - swaggerParameters = "parameters" - swaggerHeaders = "headers" - swaggerOperationID = "operationId" + swaggerPaths = "paths" + swaggerDefinitions = "definitions" + swaggerResponses = "responses" + swaggerParameters = "parameters" + swaggerHeaders = "headers" + swaggerOperationID = "operationId" + swaggerSecurity = "security" + swaggerCollectionFormat = "collectionFormat" + + // securitySchemeOAuth2 is the only security scheme type whose requirements carry scopes. + securitySchemeOAuth2 = "oauth2" jsonMimeApplicationJSON = "application/json" ) diff --git a/vendor/github.com/go-openapi/validate/object_validator.go b/vendor/github.com/go-openapi/validate/object_validator.go index 4f1dd15..cac243e 100644 --- a/vendor/github.com/go-openapi/validate/object_validator.go +++ b/vendor/github.com/go-openapi/validate/object_validator.go @@ -375,7 +375,7 @@ func (o *objectValidator) validatePropertiesSchema(val map[string]any, res *Resu } // located on the object that lacks the property: the property itself - // has no node to point at, and the object is what has to be amended + // has no node to point at, and the object has to be amended res.addErrorsAt(o.Path, errors.Required(o.Path.child(k).dotted(), o.In, v)) } } diff --git a/vendor/github.com/go-openapi/validate/path.go b/vendor/github.com/go-openapi/validate/path.go index 940fc4b..4b2da25 100644 --- a/vendor/github.com/go-openapi/validate/path.go +++ b/vendor/github.com/go-openapi/validate/path.go @@ -16,7 +16,7 @@ import ( // // Validators build a location by appending tokens as they descend into // properties and array items, then render it only when they report an error. -// Keeping the tokens apart until then is what makes it possible to produce a +// Keeping the tokens apart until then makes it possible to produce a // valid [RFC 6901] JSON pointer: a token is escaped when it is rendered, and // the separator can never be confused with a token that contains one. // diff --git a/vendor/github.com/go-openapi/validate/pools.go b/vendor/github.com/go-openapi/validate/pools.go index 0bc95d1..24fde6e 100644 --- a/vendor/github.com/go-openapi/validate/pools.go +++ b/vendor/github.com/go-openapi/validate/pools.go @@ -12,7 +12,7 @@ import ( // // Validation allocates a validator per schema node and a result per check, so // the same handful of types are built and thrown away constantly. Recycling -// them is what keeps validating a large specification affordable. +// them keeps validating a large specification affordable. // // Build with the "poolsdebug" tag to have every borrow and redeem tracked: // misuse then panics where it happens rather than corrupting a pool, and diff --git a/vendor/github.com/go-openapi/validate/schema.go b/vendor/github.com/go-openapi/validate/schema.go index e7af892..9b49007 100644 --- a/vendor/github.com/go-openapi/validate/schema.go +++ b/vendor/github.com/go-openapi/validate/schema.go @@ -61,6 +61,22 @@ func NewSchemaValidator(schema *spec.Schema, rootSchema any, root string, format o(opts) } + // the caller still owns this schema, and validation expands what it walks: work on a copy, + // once, here - every validator below this one is then free to expand in place + if !opts.ownSchemata && schema != nil { + cloned, err := deepCloneSchema(*schema) + if err != nil { + panic(invalidSchemaProvidedMsg(err).Error()) + } + + if rootSchema == schema { + rootSchema = &cloned + } + + schema = &cloned + opts.ownSchemata = true + } + return newSchemaValidator(schema, rootSchema, rootPathFromString(root), formats, opts) } diff --git a/vendor/github.com/go-openapi/validate/schema_option.go b/vendor/github.com/go-openapi/validate/schema_option.go index 3ca489c..09a03eb 100644 --- a/vendor/github.com/go-openapi/validate/schema_option.go +++ b/vendor/github.com/go-openapi/validate/schema_option.go @@ -18,6 +18,15 @@ type SchemaValidatorOptions struct { recycleResult bool skipSchemataResult bool pathLoaderWithOptions func(string, ...loading.Option) (json.RawMessage, error) + + // ownSchemata tells the validators that the schemata they walk are theirs to rewrite. + // + // Validation expands every schema carrying a $ref or an id, and expansion rewrites what it is + // given. The copy that makes this safe is taken once, at the entry point - the whole document + // in [SpecValidator.Validate], the single schema in [NewSchemaValidator] - because a copy per + // schema multiplies with the recursion, and because parameters, path items and responses + // expand too, not only schemata. + ownSchemata bool } // Option sets optional rules for schema validation. diff --git a/vendor/github.com/go-openapi/validate/security_requirements.go b/vendor/github.com/go-openapi/validate/security_requirements.go new file mode 100644 index 0000000..b6c10f3 --- /dev/null +++ b/vendor/github.com/go-openapi/validate/security_requirements.go @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package validate + +import ( + "strings" + + "github.com/go-openapi/spec" +) + +// validateSecurityRequirements checks the security requirements declared by the document and by +// each of its operations against the security definitions. +// +// A requirement names a security scheme and lists the scopes an operation needs from it: +// +// security: +// - petstore_auth: [ "write:pets" ] +// - api_key: [] +// +// Three rules apply: +// +// - the name must be declared in securityDefinitions (error) +// - only an oauth2 requirement carries scopes; every other scheme type must list none (error) +// - an oauth2 requirement should only name scopes its scheme declares (warning) +// +// The JSON meta-schema types a requirement as an object of string arrays and never reads +// securityDefinitions, so it can express none of the three. +// +// An empty requirement object ({}) names no scheme and passes. So does an empty security array, +// which an operation uses to drop the requirements the document sets for every operation. +func (s *SpecValidator) validateSecurityRequirements() *Result { + res := validatorPools.results.Borrow() + definitions := s.spec.Spec().SecurityDefinitions + + res.Merge(checkSecurityRequirements( + newPathSegments(swaggerSecurity), + s.spec.Spec().Security, + definitions, + )) + + operations := s.expandedAnalyzer().Operations() + for _, method := range sortedKeys(operations) { + byPath := operations[method] + for _, path := range sortedKeys(byPath) { + op := byPath[path] + if op == nil { + continue + } + + res.Merge(checkSecurityRequirements( + operationPath(path, method).child(swaggerSecurity), + op.Security, + definitions, + )) + } + } + + return res +} + +// checkSecurityRequirements checks the list of security requirements held at the given location. +// +// Requirements are checked in the order the document lists them, and the schemes one requirement +// names in sorted order: a requirement is a map, so the document's own order is lost (see +// [sortedKeys]). +func checkSecurityRequirements(at pathSegments, requirements []map[string][]string, definitions spec.SecurityDefinitions) *Result { + res := validatorPools.results.Borrow() + + for i, requirement := range requirements { + for _, name := range sortedKeys(requirement) { + scopes := requirement[name] + schemeAt := at.item(i).child(name) + + scheme, isDeclared := definitions[name] + if !isDeclared || scheme == nil { + res.addErrorsAt(schemeAt, securitySchemeNotDeclaredMsg(name)) + + continue + } + + if scheme.Type != securitySchemeOAuth2 { + if len(scopes) > 0 { + res.addErrorsAt(schemeAt, securityScopesNotEmptyMsg(name, strings.Join(scopes, ", "), scheme.Type)) + } + + continue + } + + for _, scope := range scopes { + if _, isKnown := scheme.Scopes[scope]; !isKnown { + res.addWarningsAt(schemeAt, securityScopeNotDeclaredMsg(name, scope)) + } + } + } + } + + return res +} diff --git a/vendor/github.com/go-openapi/validate/spec.go b/vendor/github.com/go-openapi/validate/spec.go index e5d16b6..1337523 100644 --- a/vendor/github.com/go-openapi/validate/spec.go +++ b/vendor/github.com/go-openapi/validate/spec.go @@ -4,13 +4,10 @@ package validate import ( - "bytes" - "encoding/gob" "encoding/json" "fmt" "slices" "sort" - "strconv" "strings" "github.com/go-openapi/analysis" @@ -31,7 +28,6 @@ import ( // // - Proposal for enhancement: $ref should not have siblings // - Proposal for enhancement: make sure documentation reflects all checks and warnings -// - Proposal for enhancement: check on discriminators // - Proposal for enhancement: explicit message on unsupported keywords (better than "forbidden property"...) // - Proposal for enhancement: full list of unresolved refs // - Proposal for enhancement: validate numeric constraints (issue#581): this should be handled like defaults and examples @@ -101,6 +97,17 @@ func (s *SpecValidator) Validate(data any) (*Result, *Result) { errs.AddErrors(invalidDocumentMsg()) return errs, warnings // no point in continuing } + + // Validation expands what it walks - schemata, but also parameters, path items and responses - + // and expansion rewrites what it is given. Take one copy of the whole document here and work + // on that, so the caller gets back the document it handed over. Raw() still reads the bytes as + // they were authored, so the checks below that go through them are unaffected. + // + // Cloning here rather than per schema is what keeps the cost flat: a copy taken inside + // newSchemaValidator is paid again at every level of a recursive document. + raw := sd.Raw() + sd = sd.Pristine() + s.schemaOptions.ownSchemata = true s.spec = sd s.analyzer = analysis.New(sd.Spec()) // where each $ref sits, as authored: refs are reported against the @@ -115,7 +122,7 @@ func (s *SpecValidator) Validate(data any) (*Result, *Result) { // Raw spec unmarshalling errors var obj any - if err := json.Unmarshal(sd.Raw(), &obj); err != nil { + if err := json.Unmarshal(raw, &obj); err != nil { // NOTE: under normal conditions, the *load.Document has been already unmarshalled // So this one is just a paranoid check on the behavior of the spec package panic(InvalidDocumentError) @@ -153,6 +160,9 @@ func (s *SpecValidator) Validate(data any) (*Result, *Result) { errs.Merge(s.validateDuplicatePropertyNames()) // error - errs.Merge(s.validateParameters()) // error - errs.Merge(s.validateItems()) // error - + errs.Merge(s.validateSecurityRequirements()) // error and warning + errs.Merge(s.validateDiscriminators()) // error - + errs.Merge(s.validateCollectionFormats()) // warning only // Properties in required definition MUST validate their schema // Properties SHOULD NOT be declared as both required and readOnly (warning) @@ -190,7 +200,7 @@ func (s *SpecValidator) SetContinueOnErrors(c bool) { func (s *SpecValidator) validateNonEmptyPathParamNames() *Result { res := validatorPools.results.Borrow() if s.spec.Spec().Paths == nil { - // There is no Paths object: the document itself is what lacks it, so + // There is no Paths object: the document itself lacks it, so // there is no node below it to point at res.addErrorsAt(rootPath(), noValidPathMsg()) @@ -458,26 +468,7 @@ func (s *SpecValidator) validateItems() *Result { } } - type codedResponse struct { - code string - resp spec.Response - } - var responses []codedResponse - if op.Responses != nil { - if op.Responses.Default != nil { - responses = append(responses, codedResponse{code: jsonDefault, resp: *op.Responses.Default}) - } - if op.Responses.StatusCodeResponses != nil { - for _, code := range sortedKeys(op.Responses.StatusCodeResponses) { - responses = append(responses, codedResponse{ - code: strconv.Itoa(code), - resp: op.Responses.StatusCodeResponses[code], - }) - } - } - } - - for _, resp := range responses { + for _, resp := range responsesOf(op) { at := responsePath(path, method, resp.code) // Response headers with array for _, hn := range sortedKeys(resp.resp.Headers) { @@ -969,14 +960,15 @@ func (s *SpecValidator) expandedAnalyzer() *analysis.Spec { return s.analyzer } +// deepCloneSchema returns a copy of src that shares nothing with it. +// +// The copy goes through JSON, which is the form [spec.Schema] is defined by. gob drops any field +// holding its zero value and flattens a pointer to what it points at, so a *float64 pointing at +// 0 - "minimum": 0, which the JSON Schema meta-schema spells for every positiveInteger - came +// back nil and the bound was lost. JSON is also the faster of the two on this model. func deepCloneSchema(src spec.Schema) (spec.Schema, error) { - var b bytes.Buffer - if err := gob.NewEncoder(&b).Encode(src); err != nil { - return spec.Schema{}, err - } - var dst spec.Schema - if err := gob.NewDecoder(&b).Decode(&dst); err != nil { + if err := jsonutils.FromDynamicJSON(src, &dst); err != nil { return spec.Schema{}, err } diff --git a/vendor/github.com/go-openapi/validate/spec_messages.go b/vendor/github.com/go-openapi/validate/spec_messages.go index 0a0739a..541af4d 100644 --- a/vendor/github.com/go-openapi/validate/spec_messages.go +++ b/vendor/github.com/go-openapi/validate/spec_messages.go @@ -44,6 +44,15 @@ const ( // DefaultValueInDoesNotValidateError ... DefaultValueInDoesNotValidateError = "in operation %q, default value in %s does not validate its schema" + // DiscriminatorNotDefinedError indicates a schema whose discriminator names a property the + // schema does not declare. A discriminator tells subtypes apart by the value of that property, + // so an instance has nowhere to carry the value when the property is not declared. + DiscriminatorNotDefinedError = "discriminator %q of %q is not defined as a property of that schema" + + // DiscriminatorNotRequiredError indicates a schema whose discriminator property is declared but + // left optional. An instance that omits it cannot be resolved to a subtype. + DiscriminatorNotRequiredError = "discriminator %q of %q is not in the required property list" + // DuplicateParamNameError ... DuplicateParamNameError = "duplicate parameter name %q for %q in operation %q" @@ -136,6 +145,14 @@ const ( // in the definition itself. RequiredButNotDefinedInSchemaError = "%q is present in required but not defined as property in schema %q" + // SecuritySchemeNotDeclaredError indicates a security requirement naming a scheme that + // securityDefinitions does not declare. + SecuritySchemeNotDeclaredError = "security requirement %q is not declared in securityDefinitions" + + // SecurityScopesNotEmptyError indicates a security requirement listing scopes on a scheme that is + // not oauth2. Only an oauth2 requirement carries scopes; every other type must list none. + SecurityScopesNotEmptyError = "security requirement %q lists scopes (%s), but the security scheme it names is of type %q: only oauth2 requirements carry scopes" + // SomeParametersBrokenError indicates that some parameters could not be resolved, which might result in partial checks to be carried on. SomeParametersBrokenError = "some parameters definitions are broken in %q.%s. Cannot carry on full checks on parameters for operation %s" @@ -171,6 +188,11 @@ const ( // RequiredHasDefaultWarning indicates that a required parameter property should not have a default. RequiredHasDefaultWarning = "%s in %s has a default value and is required as parameter" + // SecurityScopeNotDeclaredWarning flags an oauth2 security requirement asking for a scope that the + // scheme does not list in its scopes. Swagger 2.0 does not spell out that the two must agree, so + // this is reported as a warning: a specification that names an undeclared scope stays valid. + SecurityScopeNotDeclaredWarning = "security requirement %q requires scope %q, which the security scheme does not declare" + // UnusedDefinitionWarning ... UnusedDefinitionWarning = "definition %q is not used anywhere" @@ -180,6 +202,12 @@ const ( // UnusedResponseWarning ... UnusedResponseWarning = "response %q is not used anywhere" + // CollectionFormatIgnoredWarning flags a collectionFormat on a parameter, header or items whose + // type is not array. collectionFormat says how to join the members of an array into one value, so + // it does nothing anywhere else. It is a warning, not an error: Swagger 2.0 defines the member as + // applying when the type is array, and does not forbid writing it elsewhere. + CollectionFormatIgnoredWarning = "collectionFormat %q is ignored in %s: it joins the members of an array, and the type is %q" + // DubiousAbsoluteRefWarning flags a $ref pointing to an absolute local file location that escapes the // spec's base path. Absolute local references are legitimate when they stay beneath the base path // (flattening/expansion introduces such anchors for cyclical $refs), but an absolute reference that @@ -416,6 +444,31 @@ func invalidObjectMsg(path, in string) errors.Error { // func invalidResponseDefinitionAsSchemaMsg(path, method string) errors.Error { // return errors.New(errors.CompositeErrorCode, InvalidResponseDefinitionAsSchemaError, path, method) // } + +func collectionFormatIgnoredMsg(collectionFormat, in, typ string) errors.Error { + return errors.New(errors.CompositeErrorCode, CollectionFormatIgnoredWarning, collectionFormat, in, typ) +} + +func discriminatorNotDefinedMsg(discriminator, in string) errors.Error { + return errors.New(errors.CompositeErrorCode, DiscriminatorNotDefinedError, discriminator, in) +} + +func discriminatorNotRequiredMsg(discriminator, in string) errors.Error { + return errors.New(errors.CompositeErrorCode, DiscriminatorNotRequiredError, discriminator, in) +} + +func securitySchemeNotDeclaredMsg(name string) errors.Error { + return errors.New(errors.CompositeErrorCode, SecuritySchemeNotDeclaredError, name) +} + +func securityScopesNotEmptyMsg(name, scopes, schemeType string) errors.Error { + return errors.New(errors.CompositeErrorCode, SecurityScopesNotEmptyError, name, scopes, schemeType) +} + +func securityScopeNotDeclaredMsg(name, scope string) errors.Error { + return errors.New(errors.CompositeErrorCode, SecurityScopeNotDeclaredWarning, name, scope) +} + func someParametersBrokenMsg(path, method, operationID string) errors.Error { return errors.New(errors.CompositeErrorCode, SomeParametersBrokenError, path, method, operationID) } diff --git a/vendor/github.com/go-openapi/validate/update-fixtures.sh b/vendor/github.com/go-openapi/validate/update-fixtures.sh deleted file mode 100644 index 8ee55ca..0000000 --- a/vendor/github.com/go-openapi/validate/update-fixtures.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -# SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers -# SPDX-License-Identifier: Apache-2.0 - -set -eu -o pipefail -dir=$(git rev-parse --show-toplevel) -scratch=$(mktemp -d -t tmp.XXXXXXXXXX) - -function finish { - rm -rf "$scratch" -} -trap finish EXIT SIGHUP SIGINT SIGTERM - -cd "$scratch" -git clone https://github.com/json-schema-org/JSON-Schema-Test-Suite Suite -cp -r Suite/tests/draft4/* "$dir/fixtures/jsonschema_suite" -cp -a Suite/remotes "$dir/fixtures/jsonschema_suite" diff --git a/vendor/modules.txt b/vendor/modules.txt index b754953..61dcda1 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -8,7 +8,7 @@ github.com/go-logr/logr/funcr # github.com/go-logr/stdr v1.2.2 ## explicit; go 1.16 github.com/go-logr/stdr -# github.com/go-openapi/analysis v0.26.0 +# github.com/go-openapi/analysis v0.26.1 ## explicit; go 1.25.0 github.com/go-openapi/analysis github.com/go-openapi/analysis/internal/debug @@ -48,7 +48,7 @@ github.com/go-openapi/runtime/server-middleware/docui github.com/go-openapi/runtime/server-middleware/mediatype github.com/go-openapi/runtime/server-middleware/negotiate github.com/go-openapi/runtime/server-middleware/negotiate/header -# github.com/go-openapi/spec v0.22.9 +# github.com/go-openapi/spec v0.22.10 ## explicit; go 1.25.0 github.com/go-openapi/spec # github.com/go-openapi/strfmt v0.27.0 @@ -59,7 +59,7 @@ github.com/go-openapi/strfmt/internal/countries # github.com/go-openapi/swag/conv v0.29.1 ## explicit; go 1.25.0 github.com/go-openapi/swag/conv -# github.com/go-openapi/swag/fileutils v0.28.0 +# github.com/go-openapi/swag/fileutils v0.29.1 ## explicit; go 1.25.0 github.com/go-openapi/swag/fileutils # github.com/go-openapi/swag/jsonutils v0.29.1 @@ -68,10 +68,10 @@ github.com/go-openapi/swag/jsonutils github.com/go-openapi/swag/jsonutils/adapters github.com/go-openapi/swag/jsonutils/adapters/ifaces github.com/go-openapi/swag/jsonutils/adapters/stdlib/json -# github.com/go-openapi/swag/loading v0.28.0 +# github.com/go-openapi/swag/loading v0.29.1 ## explicit; go 1.25.0 github.com/go-openapi/swag/loading -# github.com/go-openapi/swag/mangling v0.28.0 +# github.com/go-openapi/swag/mangling v0.29.1 ## explicit; go 1.25.0 github.com/go-openapi/swag/mangling # github.com/go-openapi/swag/pools v0.29.1 @@ -83,10 +83,10 @@ github.com/go-openapi/swag/stringutils # github.com/go-openapi/swag/typeutils v0.29.1 ## explicit; go 1.25.0 github.com/go-openapi/swag/typeutils -# github.com/go-openapi/swag/yamlutils v0.28.0 +# github.com/go-openapi/swag/yamlutils v0.29.1 ## explicit; go 1.25.0 github.com/go-openapi/swag/yamlutils -# github.com/go-openapi/validate v0.26.3 +# github.com/go-openapi/validate v0.26.4 ## explicit; go 1.25.0 github.com/go-openapi/validate # github.com/go-viper/mapstructure/v2 v2.5.0