diff --git a/go.mod b/go.mod index 400c98ec4..84365dd21 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/go-openapi/errors v0.22.8 github.com/go-openapi/runtime v0.32.4 github.com/go-openapi/strfmt v0.26.4 - github.com/go-openapi/swag v0.27.0 + github.com/go-openapi/swag v0.27.3 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/go-github/v84 v84.0.0 github.com/google/uuid v1.6.0 @@ -59,22 +59,23 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.25.3 // indirect - github.com/go-openapi/jsonpointer v0.24.0 // indirect + github.com/go-openapi/jsonpointer v1.0.0 // indirect github.com/go-openapi/jsonreference v0.21.6 // indirect github.com/go-openapi/loads v0.24.0 // indirect github.com/go-openapi/runtime/server-middleware v0.32.4 // indirect github.com/go-openapi/spec v0.22.6 // indirect - github.com/go-openapi/swag/cmdutils v0.27.0 // indirect - github.com/go-openapi/swag/conv v0.27.0 // indirect - github.com/go-openapi/swag/fileutils v0.27.0 // indirect + github.com/go-openapi/swag/cmdutils v0.27.3 // indirect + github.com/go-openapi/swag/conv v0.27.3 // indirect + github.com/go-openapi/swag/fileutils v0.27.3 // indirect github.com/go-openapi/swag/jsonname v0.27.0 // indirect - github.com/go-openapi/swag/jsonutils v0.27.0 // indirect - github.com/go-openapi/swag/loading v0.27.0 // indirect - github.com/go-openapi/swag/mangling v0.27.0 // indirect - github.com/go-openapi/swag/netutils v0.27.0 // indirect - github.com/go-openapi/swag/stringutils v0.27.0 // indirect - github.com/go-openapi/swag/typeutils v0.27.0 // indirect - github.com/go-openapi/swag/yamlutils v0.27.0 // indirect + github.com/go-openapi/swag/jsonutils v0.27.3 // indirect + github.com/go-openapi/swag/loading v0.27.3 // indirect + github.com/go-openapi/swag/mangling v0.27.3 // indirect + github.com/go-openapi/swag/netutils v0.27.3 // indirect + github.com/go-openapi/swag/pools v0.27.3 // indirect + github.com/go-openapi/swag/stringutils v0.27.3 // indirect + github.com/go-openapi/swag/typeutils v0.27.3 // indirect + github.com/go-openapi/swag/yamlutils v0.27.3 // indirect github.com/go-openapi/validate v0.26.0 // indirect github.com/go-sql-driver/mysql v1.10.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect diff --git a/go.sum b/go.sum index c0237c8f2..29d62056f 100644 --- a/go.sum +++ b/go.sum @@ -52,8 +52,8 @@ github.com/go-openapi/analysis v0.25.3 h1:4zlcg85pd2xq3sEgjW887n1IpwCpCqTmqeT6dP github.com/go-openapi/analysis v0.25.3/go.mod h1:6PEmUIra9/rn6SPstzbrMkhFAsMB2qm7g6E+4DRFyCU= 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 v0.24.0 h1:AA6mCjHYHmZ+1RU2Js089EaOK/iwXXNwQsTgnsTha2M= -github.com/go-openapi/jsonpointer v0.24.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= +github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s= +github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= github.com/go-openapi/jsonreference v0.21.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y= github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY= github.com/go-openapi/loads v0.24.0 h1:4LLorXRPTzIN9V6ngMUZbAscsBOUBk3Oa8cClu/bFrQ= @@ -66,32 +66,34 @@ github.com/go-openapi/spec v0.22.6 h1:Tyy1pLaNCM8GBCFLoGYLonjJi6zykqyLCjXLc19ZPi github.com/go-openapi/spec v0.22.6/go.mod h1:HZvTHat+iH0PALQRWhrqIHtU/PEqxqd89fu0MxGlMeM= github.com/go-openapi/strfmt v0.26.4 h1:yI6IAEfcWow459BD5UzFY430KUwXZwBHrYusPFkhWlc= github.com/go-openapi/strfmt v0.26.4/go.mod h1:hNJi6nb5ETD6i7A1yRo03M9S6ZoTPPoWff1iUexmfUc= -github.com/go-openapi/swag v0.27.0 h1:8ecSuZlh4NXc3GsmAOqECIYqDTApCWaMe3gO4gjJNEE= -github.com/go-openapi/swag v0.27.0/go.mod h1:Kkgz9Ht0+ul9/aVdFmc9xSyPzUwf/aFF5KiFPBXfSY0= -github.com/go-openapi/swag/cmdutils v0.27.0 h1:aIKiqhB29AaP+7xm8/CPg3uOpeHx2SUp6TvMpu/a31Y= -github.com/go-openapi/swag/cmdutils v0.27.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= -github.com/go-openapi/swag/conv v0.27.0 h1:EKOH4feXrvdo8DbSsXSAqRT8fz1epEnS5O2IfXUOzE8= -github.com/go-openapi/swag/conv v0.27.0/go.mod h1:pfiv0uKQTbaGApk8Zs/lZV3uSjmSpa2FO1y183YngN8= -github.com/go-openapi/swag/fileutils v0.27.0 h1:ib5jMUqGq5tY1EyO4inlrabsaeDAleFU+XD1FXQcgp8= -github.com/go-openapi/swag/fileutils v0.27.0/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8= +github.com/go-openapi/swag v0.27.3 h1:i6oVKkGZeFgETHMiBHGtj9gIQ1aLtWDdJnT/SRZeets= +github.com/go-openapi/swag v0.27.3/go.mod h1:qEXs3GcyyQTDCFQ4ykqnLPDh8qT+zBcjbVWdxCAW0Us= +github.com/go-openapi/swag/cmdutils v0.27.3 h1:sjuL0TvW81i9R9GRMO/fy+c3mOW+7zxRYwy/7fobZt4= +github.com/go-openapi/swag/cmdutils v0.27.3/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= +github.com/go-openapi/swag/conv v0.27.3 h1:iqJFmGEjmX3AY0lSszABFqRVqOSt99XS0LzNIMJYuhU= +github.com/go-openapi/swag/conv v0.27.3/go.mod h1:nPRmN6jgNme99hpf+nM0auDZGALWIqlwhisKPK/bQhQ= +github.com/go-openapi/swag/fileutils v0.27.3 h1:3UVoZ2RLaIs1lt+2jcKzL8RM3Yk0rmsDE9FLA/HGxFE= +github.com/go-openapi/swag/fileutils v0.27.3/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8= github.com/go-openapi/swag/jsonname v0.27.0 h1:4QVB//CKOdE8IOiBg19JNY2wfDS48MhesIquYBy2rUE= github.com/go-openapi/swag/jsonname v0.27.0/go.mod h1:I1YsyvvhBuZsFXSW6I7ODfdyq13p7hDil//1T9/pFFk= -github.com/go-openapi/swag/jsonutils v0.27.0 h1:VYtd9jEQYeU4j8q5vdn5KWotF4vKywhGdMBrALtAsfE= -github.com/go-openapi/swag/jsonutils v0.27.0/go.mod h1:U7pb8AGuwhok3RDicHeHwSG4L3PXSq6PAL98Aon632g= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.0 h1:+d7C7Ur/SsGg/UZ9G0JEovnfRqtMNZCJQGKc2h/ojoE= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.0/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY= -github.com/go-openapi/swag/loading v0.27.0 h1:s8DA9aPEdFH6OluHUYUn3DnIuoTdyWs9RwffXBUfyeI= -github.com/go-openapi/swag/loading v0.27.0/go.mod h1:VOz+Jg6UGGywcmRvYsI4fvtp+bd7NfioseGEPleYdA4= -github.com/go-openapi/swag/mangling v0.27.0 h1:rpPJuqQHa6z2pDiP3iIpXOyNXlSs9cQCxnJSAxzdfOc= -github.com/go-openapi/swag/mangling v0.27.0/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w= -github.com/go-openapi/swag/netutils v0.27.0 h1:lEUG+hHvPvLggB3A8snFk0IRKNf9uC0YKc+7WYqvAF8= -github.com/go-openapi/swag/netutils v0.27.0/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ= -github.com/go-openapi/swag/stringutils v0.27.0 h1:Of7w/HljWsNZvuxsUAnw3n+hCOyI6HLJOxW2kQRAxio= -github.com/go-openapi/swag/stringutils v0.27.0/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM= -github.com/go-openapi/swag/typeutils v0.27.0 h1:aCf4MSGo8NLwZP8Q6t32DWLJSvl/WwNqgmEG+xJ6v2o= -github.com/go-openapi/swag/typeutils v0.27.0/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= -github.com/go-openapi/swag/yamlutils v0.27.0 h1:bQ6eAMil5X9tdcf7dMn4t15alzG6jddnrKPuKa/zxKM= -github.com/go-openapi/swag/yamlutils v0.27.0/go.mod h1:yRfIo7qqVkmJRQjX8exjA3AfcI8rH1KDNPsTparoCv4= +github.com/go-openapi/swag/jsonutils v0.27.3 h1:1DEz+O82frtSMBcos/7XIn1GnpNTbsD4Bru4Dc/uhRc= +github.com/go-openapi/swag/jsonutils v0.27.3/go.mod h1:qiDCoQvzkMxrV3G8FLEdIU5L+EFYc0zcDOHWT3Yofvo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3 h1:h/eT9kmGCDdFLJF29lOhzLtF0FmP1AX2MhLJWVebsb8= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY= +github.com/go-openapi/swag/loading v0.27.3 h1:L9nQkEgzU7QgFQL+pLEMfGUKxeM4pWwGwbET9Z3weW0= +github.com/go-openapi/swag/loading v0.27.3/go.mod h1:rJ0NeaKsF4CVPnMGjPQl7JlSHzvD0bc2DKXLss1hiuE= +github.com/go-openapi/swag/mangling v0.27.3 h1:gRzzD1PAUoLTtGMgI3KpBmCSOlTuLTFWnviLxLcTnyg= +github.com/go-openapi/swag/mangling v0.27.3/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w= +github.com/go-openapi/swag/netutils v0.27.3 h1:IoBvfCoprsE6E87kAIm9basnISqDDqB79mJ8MN+f5PU= +github.com/go-openapi/swag/netutils v0.27.3/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ= +github.com/go-openapi/swag/pools v0.27.3 h1:gXjImP3F6/56wRRcFgEPld084Y6u2gs21ikPBt8NKBk= +github.com/go-openapi/swag/pools v0.27.3/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE= +github.com/go-openapi/swag/stringutils v0.27.3 h1:Ru28hnbAvN5wycALQYy8IobHvASq+FUFMlp1QzLM0JI= +github.com/go-openapi/swag/stringutils v0.27.3/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM= +github.com/go-openapi/swag/typeutils v0.27.3 h1:l6SSrx5eR5/WVwrGNzN6bQ9WqL04mrxNBl9YgQ3rcJ4= +github.com/go-openapi/swag/typeutils v0.27.3/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= +github.com/go-openapi/swag/yamlutils v0.27.3 h1:cRFCAoYtslYn9L9T0xWryHy1t7c1MACC+DMj3CLvwvs= +github.com/go-openapi/swag/yamlutils v0.27.3/go.mod h1:6JYBGj8sw/NawMllyZY+cTA8Mzk2etS3ZBASdcyPsiU= github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0= github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo= github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug= diff --git a/vendor/github.com/go-openapi/jsonpointer/CONTRIBUTORS.md b/vendor/github.com/go-openapi/jsonpointer/CONTRIBUTORS.md index 0cdcfb4ce..de0bf5c04 100644 --- a/vendor/github.com/go-openapi/jsonpointer/CONTRIBUTORS.md +++ b/vendor/github.com/go-openapi/jsonpointer/CONTRIBUTORS.md @@ -4,21 +4,21 @@ | Total Contributors | Total Contributions | | --- | --- | -| 13 | 115 | +| 13 | 132 | | Username | All Time Contribution Count | All Commits | | --- | --- | --- | -| @fredbi | 67 | | +| @fredbi | 83 | | | @casualjim | 33 | | | @magodo | 3 | | | @youyuanwu | 3 | | +| @alexandear | 2 | | | @gaiaz-iusipov | 1 | | | @gbjk | 1 | | | @gordallott | 1 | | | @ianlancetaylor | 1 | | | @mfleader | 1 | | | @Neo2308 | 1 | | -| @alexandear | 1 | | | @olivierlemasle | 1 | | | @testwill | 1 | | diff --git a/vendor/github.com/go-openapi/jsonpointer/README.md b/vendor/github.com/go-openapi/jsonpointer/README.md index a90e79935..6e7929c64 100644 --- a/vendor/github.com/go-openapi/jsonpointer/README.md +++ b/vendor/github.com/go-openapi/jsonpointer/README.md @@ -16,6 +16,9 @@ An implementation of JSON Pointer for golang, which supports go `struct`. ## Announcements +* **2026-07-07** : landing v1.0.0 + * stable API pledge + * **2026-06-29** : reinsourced external dependency to swag (v0.24.0) * module `github.com/go-openapi/swag/jsonname` is source directly here, so we no longer have any external dependency * `jsonname` was never really used by any other package, so it makes sense to deprecate it away from the `swag` family diff --git a/vendor/github.com/go-openapi/swag/CONTRIBUTORS.md b/vendor/github.com/go-openapi/swag/CONTRIBUTORS.md index b619a01df..0f0711cd5 100644 --- a/vendor/github.com/go-openapi/swag/CONTRIBUTORS.md +++ b/vendor/github.com/go-openapi/swag/CONTRIBUTORS.md @@ -4,11 +4,11 @@ | Total Contributors | Total Contributions | | --- | --- | -| 24 | 247 | +| 24 | 251 | | Username | All Time Contribution Count | All Commits | | --- | --- | --- | -| @fredbi | 117 | | +| @fredbi | 121 | | | @casualjim | 98 | | | @alexandear | 4 | | | @orisano | 3 | | diff --git a/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/adapter.go b/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/adapter.go index 0213ff5c2..94185f79c 100644 --- a/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/adapter.go +++ b/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/adapter.go @@ -5,6 +5,7 @@ package json import ( stdjson "encoding/json" + "fmt" "github.com/go-openapi/swag/jsonutils/adapters/ifaces" "github.com/go-openapi/swag/typeutils" @@ -24,11 +25,16 @@ var ErrStdlib jsonError = "error from the JSON adapter stdlib" var _ ifaces.Adapter = &Adapter{} type Adapter struct { + options } // NewAdapter yields an [ifaces.Adapter] using the standard library. -func NewAdapter() *Adapter { - return &Adapter{} +func NewAdapter(opts ...Option) *Adapter { + var o options + + return &Adapter{ + options: buildOptions(o, opts), + } } func (a *Adapter) Marshal(value any) ([]byte, error) { @@ -40,45 +46,18 @@ func (a *Adapter) Unmarshal(data []byte, value any) error { } func (a *Adapter) OrderedMarshal(value ifaces.Ordered) ([]byte, error) { - w := poolOfWriters.Borrow() - defer func() { - poolOfWriters.Redeem(w) - }() - - if typeutils.IsNil(value) { - w.RawString("null") - - return w.BuildBytes() - } - - w.RawByte('{') - first := true - for k, v := range value.OrderedItems() { - if first { - first = false - } else { - w.RawByte(',') - } + w, redeem := poolOfWriters.BorrowWithRedeem() + defer redeem() + w.setBuf() - w.String(k) - w.RawByte(':') - - switch val := v.(type) { - case ifaces.Ordered: - w.Raw(a.OrderedMarshal(val)) - default: - w.Raw(stdjson.Marshal(v)) - } - } - - w.RawByte('}') + a.orderedMarshal(w, value, 1) return w.BuildBytes() } func (a *Adapter) OrderedUnmarshal(data []byte, value ifaces.SetOrdered) error { var m MapSlice - if err := m.OrderedUnmarshalJSON(data); err != nil { + if err := m.orderedUnmarshalJSON(data, a.maxDepth()); err != nil { return err } @@ -112,4 +91,43 @@ func (a *Adapter) Redeem() { } func (a *Adapter) Reset() { + a.options = options{} +} + +// orderedMarshal writes value to w, tracking the container nesting depth to guard +// against stack overflow on deeply nested structures. +func (a *Adapter) orderedMarshal(w *jwriter, value ifaces.Ordered, depth int) { + if typeutils.IsNil(value) { + w.RawString("null") + + return + } + + if maxDepth := a.maxDepth(); depth > maxDepth { + w.SetErr(fmt.Errorf("maximum nesting depth of %d exceeded: %w", maxDepth, ErrStdlib)) + + return + } + + w.RawByte('{') + first := true + for k, v := range value.OrderedItems() { + if first { + first = false + } else { + w.RawByte(',') + } + + w.String(k) + w.RawByte(':') + + switch val := v.(type) { + case ifaces.Ordered: + a.orderedMarshal(w, val, depth+1) + default: + w.Raw(stdjson.Marshal(v)) + } + } + + w.RawByte('}') } diff --git a/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/lexer.go b/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/lexer.go index b5aa1c797..ac81cbc75 100644 --- a/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/lexer.go +++ b/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/lexer.go @@ -54,7 +54,7 @@ func (t token) Delim() byte { return 0 } - return byte(r) + return byte(r) //nolint:gosec // delimiter runes are single byte } type tokenKind uint8 @@ -91,6 +91,13 @@ type jlexer struct { // current token next token // started bool + + // depth tracks the current JSON container nesting level, and maxDepth caps it + // to guard against stack-overflow on adversarially deep documents. The standard + // library's streaming [encoding/json.Decoder.Token] API (used here) does not + // enforce the max-depth guard that [encoding/json.Unmarshal] provides, so we do. + depth int + maxDepth int } type bytesReader struct { @@ -130,7 +137,8 @@ var _ io.Reader = &bytesReader{} func newLexer(data []byte) *jlexer { l := &jlexer{ // current: undefToken, - next: undefToken, + next: undefToken, + maxDepth: defaultMaxNestingDepth, } l.buf = &bytesReader{ buf: data, @@ -143,7 +151,11 @@ func newLexer(data []byte) *jlexer { func (l *jlexer) Reset() { l.err = nil l.next = undefToken - // leave l.dec and l.buf alone, since they are replaced at every Borrow + l.depth = 0 + l.maxDepth = defaultMaxNestingDepth + l.dec = nil + // leave l.buf alone, since they are replaced at every Borrow + l.buf = nil } func (l *jlexer) Error() error { @@ -228,6 +240,21 @@ func (l *jlexer) Delim(c byte) { if tok.Delim() != c { l.err = fmt.Errorf("expected delimiter '%q' but got '%q': %w", c, tok.Delim(), ErrStdlib) + + return + } + + // Track container nesting depth centrally: every '{' or '[' opens a level and + // every '}' or ']' closes one. This guards the mutually-recursive unmarshal + // routines (unmarshalObject/unmarshalArray/asInterface) against stack overflow. + switch c { + case '{', '[': + l.depth++ + if l.maxDepth > 0 && l.depth > l.maxDepth { + l.err = fmt.Errorf("maximum nesting depth of %d exceeded: %w", l.maxDepth, ErrStdlib) + } + case '}', ']': + l.depth-- } } @@ -318,3 +345,12 @@ func (l *jlexer) fetchToken() token { return token{Token: jtok} } + +func (l *jlexer) setBuf(data []byte) func() { + rdr, redeemBuf := poolOfReaders.BorrowWithRedeem() + l.buf = rdr + l.buf.buf = data + l.dec = stdjson.NewDecoder(l.buf) // cannot pool, not exposed by the encoding/json API + + return redeemBuf +} diff --git a/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/options.go b/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/options.go new file mode 100644 index 000000000..f114f1f1a --- /dev/null +++ b/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/options.go @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package json + +// defaultMaxNestingDepth is the default maximum number of nested JSON containers +// ('{' or '[') that the ordered-JSON marshaler and unmarshaler will process before +// returning an error. +// +// It mirrors the limit enforced by the standard library's [encoding/json] decoder +// (see encoding/json's internal maxNestingDepth), which this adapter would otherwise +// not benefit from since it drives [encoding/json.Decoder.Token] directly. +const defaultMaxNestingDepth = 10000 + +// Option selects options for the stdlib adapter. +type Option func(o options) options + +type options struct { + maxNestingDepth int +} + +func buildOptions(o options, opts []Option) options { + for _, apply := range opts { + o = apply(o) + } + + return o +} + +// maxDepth returns the configured maximum nesting depth, or the default when unset. +func (o options) maxDepth() int { + if o.maxNestingDepth <= 0 { + return defaultMaxNestingDepth + } + + return o.maxNestingDepth +} + +// WithMaxNestingDepth sets the maximum number of nested JSON containers accepted +// when marshaling or unmarshaling ordered JSON. +// +// A value <= 0 selects the default (10,000). +// +// This guards against stack-overflow crashes on deeply nested (possibly adversarial) +// JSON documents or in-memory structures. +func WithMaxNestingDepth(depth int) Option { + return func(o options) options { + o.maxNestingDepth = depth + + return o + } +} diff --git a/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/ordered_map.go b/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/ordered_map.go index 54deef406..a5a8f4b63 100644 --- a/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/ordered_map.go +++ b/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/ordered_map.go @@ -70,12 +70,11 @@ func (s MapSlice) MarshalJSON() ([]byte, error) { } func (s MapSlice) OrderedMarshalJSON() ([]byte, error) { - w := poolOfWriters.Borrow() - defer func() { - poolOfWriters.Redeem(w) - }() + w, redeem := poolOfWriters.BorrowWithRedeem() + defer redeem() + w.setBuf() - s.marshalObject(w) + s.marshalObject(w, 1) return w.BuildBytes() // this clones data, so it's okay to redeem the writer and its buffer } @@ -88,23 +87,38 @@ func (s *MapSlice) UnmarshalJSON(data []byte) error { } func (s *MapSlice) OrderedUnmarshalJSON(data []byte) error { - l := poolOfLexers.Borrow(data) - defer func() { - poolOfLexers.Redeem(l) - }() + return s.orderedUnmarshalJSON(data, defaultMaxNestingDepth) +} + +func (s *MapSlice) orderedUnmarshalJSON(data []byte, maxDepth int) error { + l, redeem := poolOfLexers.BorrowWithRedeem() + defer redeem() + + redeemBuf := l.setBuf(data) + defer redeemBuf() + + if maxDepth > 0 { + l.maxDepth = maxDepth + } s.unmarshalObject(l) return l.Error() } -func (s MapSlice) marshalObject(w *jwriter) { +func (s MapSlice) marshalObject(w *jwriter, depth int) { if s == nil { w.RawString("null") return } + if depth > defaultMaxNestingDepth { + w.SetErr(fmt.Errorf("maximum nesting depth of %d exceeded: %w", defaultMaxNestingDepth, ErrStdlib)) + + return + } + w.RawByte('{') if len(s) == 0 { @@ -113,11 +127,11 @@ func (s MapSlice) marshalObject(w *jwriter) { return } - s[0].marshalJSON(w) + s[0].marshalJSON(w, depth) for i := 1; i < len(s); i++ { w.RawByte(',') - s[i].marshalJSON(w) + s[i].marshalJSON(w, depth) } w.RawByte('}') @@ -162,9 +176,18 @@ type MapItem struct { Value any } -func (s MapItem) marshalJSON(w *jwriter) { +func (s MapItem) marshalJSON(w *jwriter, depth int) { w.String(s.Key) w.RawByte(':') + + // Recurse internally for nested ordered maps so the depth guard is not lost across + // the stdjson.Marshal boundary (which would reset it and re-enable stack overflow). + if nested, ok := s.Value.(MapSlice); ok { + nested.marshalObject(w, depth+1) + + return + } + w.Raw(stdjson.Marshal(s.Value)) } diff --git a/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/pool.go b/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/pool.go index 709b97c30..2f06b88e8 100644 --- a/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/pool.go +++ b/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/pool.go @@ -4,118 +4,15 @@ package json import ( - "encoding/json" - "sync" - "github.com/go-openapi/swag/jsonutils/adapters/ifaces" + "github.com/go-openapi/swag/pools" ) -type adaptersPool struct { - sync.Pool -} - -func (p *adaptersPool) Borrow() *Adapter { - return p.Get().(*Adapter) -} - -func (p *adaptersPool) BorrowIface() ifaces.Adapter { - return p.Get().(*Adapter) -} - -func (p *adaptersPool) Redeem(a *Adapter) { - p.Put(a) -} - -type writersPool struct { - sync.Pool -} - -func (p *writersPool) Borrow() *jwriter { - ptr := p.Get() - - jw := ptr.(*jwriter) - jw.Reset() - - return jw -} - -func (p *writersPool) Redeem(w *jwriter) { - p.Put(w) -} - -type lexersPool struct { - sync.Pool -} - -func (p *lexersPool) Borrow(data []byte) *jlexer { - ptr := p.Get() - - l := ptr.(*jlexer) - l.buf = poolOfReaders.Borrow(data) - l.dec = json.NewDecoder(l.buf) // cannot pool, not exposed by the encoding/json API - l.Reset() - - return l -} - -func (p *lexersPool) Redeem(l *jlexer) { - l.dec = nil - discard := l.buf - l.buf = nil - poolOfReaders.Redeem(discard) - p.Put(l) -} - -type readersPool struct { - sync.Pool -} - -func (p *readersPool) Borrow(data []byte) *bytesReader { - ptr := p.Get() - - b := ptr.(*bytesReader) - b.Reset() - b.buf = data - - return b -} - -func (p *readersPool) Redeem(b *bytesReader) { - p.Put(b) -} - var ( - poolOfAdapters = &adaptersPool{ - Pool: sync.Pool{ - New: func() any { - return NewAdapter() - }, - }, - } - - poolOfWriters = &writersPool{ - Pool: sync.Pool{ - New: func() any { - return newJWriter() - }, - }, - } - - poolOfLexers = &lexersPool{ - Pool: sync.Pool{ - New: func() any { - return newLexer(nil) - }, - }, - } - - poolOfReaders = &readersPool{ - Pool: sync.Pool{ - New: func() any { - return &bytesReader{} - }, - }, - } + poolOfAdapters = pools.New[Adapter]() + poolOfWriters = pools.NewRedeemable[jwriter]() + poolOfLexers = pools.NewRedeemable[jlexer]() + poolOfReaders = pools.NewRedeemable[bytesReader]() ) // BorrowAdapter borrows an [Adapter] from the pool, recycling already allocated instances. @@ -124,10 +21,12 @@ func BorrowAdapter() *Adapter { } // BorrowAdapterIface borrows a stdlib [Adapter] and converts it directly -// to [ifaces.Adapter]. This is useful to avoid further allocations when -// translating the concrete type into an interface. +// to [ifaces.Adapter]. +// +// This is useful to avoid further allocations when translating the concrete type into +// an interface. func BorrowAdapterIface() ifaces.Adapter { - return poolOfAdapters.BorrowIface() + return poolOfAdapters.Borrow() } // RedeemAdapter redeems an [Adapter] to the pool, so it may be recycled. diff --git a/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/register.go b/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/register.go index fc8818694..0dec85425 100644 --- a/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/register.go +++ b/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/register.go @@ -10,14 +10,22 @@ import ( "github.com/go-openapi/swag/jsonutils/adapters/ifaces" ) -func Register(dispatcher ifaces.Registrar) { +func Register(dispatcher ifaces.Registrar, opts ...Option) { t := reflect.TypeOf(Adapter{}) + var o options + o = buildOptions(o, opts) + dispatcher.RegisterFor( ifaces.RegistryEntry{ - Who: fmt.Sprintf("%s.%s", t.PkgPath(), t.Name()), - What: ifaces.AllCapabilities, - Constructor: BorrowAdapterIface, - Support: support, + Who: fmt.Sprintf("%s.%s", t.PkgPath(), t.Name()), + What: ifaces.AllCapabilities, + Constructor: func() ifaces.Adapter { + a := BorrowAdapter() + a.options = o + + return a + }, + Support: support, }) } diff --git a/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/writer.go b/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/writer.go index dc2325c1a..c84e02cd8 100644 --- a/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/writer.go +++ b/vendor/github.com/go-openapi/swag/jsonutils/adapters/stdlib/json/writer.go @@ -14,17 +14,21 @@ type jwriter struct { err error } -func newJWriter() *jwriter { - buf := make([]byte, 0, sensibleBufferSize) - - return &jwriter{buf: bytes.NewBuffer(buf)} -} - func (w *jwriter) Reset() { - w.buf.Reset() + if w.buf != nil { + w.buf.Reset() + } w.err = nil } +// SetErr records the first error encountered while building the JSON output. +func (w *jwriter) SetErr(err error) { + if w.err != nil { + return + } + w.err = err +} + func (w *jwriter) RawString(s string) { if w.err != nil { return @@ -73,3 +77,12 @@ func (w *jwriter) BuildBytes() ([]byte, error) { return bytes.Clone(w.buf.Bytes()), nil } + +func (w *jwriter) setBuf() { + if w.buf != nil { + return + } + + buf := make([]byte, 0, sensibleBufferSize) + w.buf = bytes.NewBuffer(buf) +} diff --git a/vendor/github.com/go-openapi/swag/loading/loading.go b/vendor/github.com/go-openapi/swag/loading/loading.go index 0b38ac1e3..b06450c64 100644 --- a/vendor/github.com/go-openapi/swag/loading/loading.go +++ b/vendor/github.com/go-openapi/swag/loading/loading.go @@ -84,7 +84,9 @@ func LoadStrategy(pth string, local, remote func(string) ([]byte, error), opts . if isFSBacked { // other fs.FS (e.g. os.DirFS) and os.Root loaders also use "/" on every platform. - // Escaping paths (absolute, "..", escaping symlinks) are rejected by the loader, not rewritten here. + // Path confinement is enforced by the loader, not here: the os.Root loader rebases + // absolute in-root paths and rejects escaping paths ("..", out-of-root absolute, + // escaping symlinks); an fs.FS loader rejects what its file system does not allow. return local(filepath.ToSlash(cpth)) } diff --git a/vendor/github.com/go-openapi/swag/loading/options.go b/vendor/github.com/go-openapi/swag/loading/options.go index 2c1282317..539987547 100644 --- a/vendor/github.com/go-openapi/swag/loading/options.go +++ b/vendor/github.com/go-openapi/swag/loading/options.go @@ -8,6 +8,8 @@ import ( "io/fs" "net/http" "os" + "path/filepath" + "runtime" "time" ) @@ -39,13 +41,21 @@ func (fo fileOptions) ReadFileFunc() func(string) ([]byte, error) { root := fo.root return func(name string) ([]byte, error) { + // os.Root only accepts paths relative to the root, but callers (and this package's + // own file:// handling) routinely produce absolute paths. Rebase an absolute path + // onto the root before handing it to os.Root. + rel, err := rootRelative(root, name) + if err != nil { + return nil, errors.Join(err, ErrLoader) + } + r, err := os.OpenRoot(root) if err != nil { return nil, errors.Join(err, ErrLoader) } defer func() { _ = r.Close() }() - return r.ReadFile(name) + return r.ReadFile(rel) } } @@ -56,6 +66,61 @@ func (fo fileOptions) ReadFileFunc() func(string) ([]byte, error) { return fo.fs.ReadFile } +// rootRelative expresses name as a path relative to root, so that it can be resolved by os.Root. +// +// A relative name is returned unchanged: os.Root confines it directly (including "../" traversal +// and symlink escapes, which it rejects at open time). +// +// An absolute name is rebased onto root. If it cannot be expressed relative to root — for +// example because it lives on a different volume on Windows — filepath.Rel returns an error, +// which is propagated so the read is rejected rather than silently escaping the root. An +// absolute path that lexically escapes root yields a "../" prefix here and is then rejected by +// os.Root. +func rootRelative(root, name string) (string, error) { + osName := toOSPath(name) + if !filepath.IsAbs(osName) { + return name, nil + } + + absRoot, err := filepath.Abs(toOSPath(root)) + if err != nil { + return "", err + } + + return filepath.Rel(absRoot, osName) +} + +// toOSPath converts a slash-separated path to an OS-native path. +// +// On Windows it additionally normalizes the "/C:/dir" form — a leading separator before a drive +// letter — that file URIs and URL-style path normalization (as performed by +// github.com/go-openapi/spec) produce. Without this, filepath.IsAbs does not recognize such a +// path as absolute and os.Root rejects an otherwise in-root target. This mirrors the file:// +// drive-letter handling in LoadStrategy, which the os.Root loader bypasses. +func toOSPath(p string) string { + p = filepath.FromSlash(p) + if runtime.GOOS == "windows" { + p = stripLeadingDriveSlash(p) + } + + return p +} + +// stripLeadingDriveSlash removes a leading separator that precedes a Windows drive letter, +// turning "\C:\dir" (from a "/C:/dir" URL path) into "C:\dir". Any other path is returned +// unchanged. It is pure (no OS dependency) so that its logic can be tested on any platform. +func stripLeadingDriveSlash(p string) string { + if len(p) >= 3 && (p[0] == '/' || p[0] == '\\') && p[2] == ':' && isASCIILetter(p[1]) { + return p[1:] + } + + return p +} + +func isASCIILetter(b byte) bool { + return ('a' <= b && b <= 'z') || ('A' <= b && b <= 'Z') +} + // WithTimeout sets a timeout for the remote file loader. // // The default timeout is 30s. @@ -123,10 +188,12 @@ func WithFS(filesystem fs.FS) Option { // WithRoot confines local file loading to dir. // -// Every requested path is resolved relative to dir, and any path that would escape dir — -// whether through an absolute path, ".." traversal, or a symlink pointing outside dir — is -// rejected. This is built on [os.Root] and is therefore resistant to the symlink escapes -// that a plain [os.DirFS] does not prevent. +// Every requested path is resolved within dir. A relative path is resolved against dir; an +// absolute path is rebased onto dir (so a caller that normalizes references to absolute paths, +// such as github.com/go-openapi/spec, still resolves correctly). Any path that would escape dir +// — through ".." traversal, an absolute path pointing outside dir, or a symlink pointing outside +// dir — is rejected. This is built on [os.Root] and is therefore resistant to the symlink +// escapes that a plain [os.DirFS] does not prevent. // // WithRoot is the recommended option when loading specs from a location derived from // untrusted input. It applies to local loading only and has no effect on remote diff --git a/vendor/github.com/go-openapi/swag/pools/LICENSE b/vendor/github.com/go-openapi/swag/pools/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/vendor/github.com/go-openapi/swag/pools/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/go-openapi/swag/pools/README.md b/vendor/github.com/go-openapi/swag/pools/README.md new file mode 100644 index 000000000..1966461cb --- /dev/null +++ b/vendor/github.com/go-openapi/swag/pools/README.md @@ -0,0 +1 @@ +# pools diff --git a/vendor/github.com/go-openapi/swag/pools/debug.go b/vendor/github.com/go-openapi/swag/pools/debug.go new file mode 100644 index 000000000..94415e989 --- /dev/null +++ b/vendor/github.com/go-openapi/swag/pools/debug.go @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package pools + +// TB is the subset of [testing.TB] used by [AssertNoLeaks]. +// +// It is satisfied by *[testing.T] and *[testing.B]. +// +// A local interface is used (rather than importing "testing") so that the +// release build does not pull the testing package — and its flags — into +// production binaries. +type TB interface { + Helper() + Errorf(format string, args ...any) + Logf(format string, args ...any) +} diff --git a/vendor/github.com/go-openapi/swag/pools/debug_off.go b/vendor/github.com/go-openapi/swag/pools/debug_off.go new file mode 100644 index 000000000..9163d6d60 --- /dev/null +++ b/vendor/github.com/go-openapi/swag/pools/debug_off.go @@ -0,0 +1,51 @@ +//go:build !poolsdebug + +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package pools + +// This is the release implementation of the pool instrumentation: it does +// nothing. +// +// tracker is an empty struct, so it adds no field to the pool types and its +// methods inline away to nothing. +// +// Build with -tags poolsdebug to get the instrumented variant (see +// debug_on.go). + +// debugBuild reports whether the pool instrumentation is compiled in (the +// poolsdebug tag). +const debugBuild = false + +// DebugBuild reports whether the pool instrumentation is compiled in (the +// poolsdebug build tag). +// +// It lets a test that must run in both modes skip the parts that are invalid +// under instrumentation — e.g. an allocation-count assertion, since the +// instrumented build allocates a per-borrow tracker. +const DebugBuild = debugBuild + +type tracker[T any] struct{} + +func (tracker[T]) register() {} + +func (tracker[T]) onBorrow(*T) {} + +func (tracker[T]) onRedeem(*T) {} + +func (tracker[T]) borrowRedeemer(_ *T, cached func()) func() { return cached } + +// AssertNoLeaks reports whether every borrowed object has been redeemed across +// all pools. +// +// It is only meaningful in the instrumented build (-tags poolsdebug). +// +// In a release build it is a no-op that always reports true, so the same test +// can run in both modes. +func AssertNoLeaks(TB) bool { return true } + +// ResetTracking clears all recorded borrow/redeem tracking. +// +// This is a no-op in a release build. +func ResetTracking() {} diff --git a/vendor/github.com/go-openapi/swag/pools/debug_on.go b/vendor/github.com/go-openapi/swag/pools/debug_on.go new file mode 100644 index 000000000..64ccc57d3 --- /dev/null +++ b/vendor/github.com/go-openapi/swag/pools/debug_on.go @@ -0,0 +1,237 @@ +//go:build poolsdebug + +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package pools + +import ( + "fmt" + "runtime" + "sync" +) + +// This is the instrumented implementation of the pool tracking, enabled with +// -tags poolsdebug. +// +// Each pool carries a tracker that records, per recycled pointer, whether it is +// currently borrowed or redeemed, together with the call sites of the last +// borrow and redeem. +// It panics loudly (with those call sites) when it detects misuse: +// +// - a double redeem (the same object returned to the pool twice — corrupts sync.Pool); +// - for the redeemable pools, a redeem of a stale borrow (the slot was re-borrowed since — the +// ABA case the production atomic guard cannot catch), thanks to a per-borrow generation; +// - a redeem of an object the pool never handed out; +// - a borrow of an object still checked out (a symptom of an earlier double-Put). +// +// Borrowed-but-never-redeemed objects (leaks) are reported by [AssertNoLeaks]. + +// debugBuild reports whether the pool instrumentation is compiled in (the +// poolsdebug tag). +const debugBuild = true + +// DebugBuild reports whether the pool instrumentation is compiled in (the +// poolsdebug build tag). +// +// See the release-build doc for usage. +const DebugBuild = debugBuild + +type trackStatus uint8 + +const ( + trackBorrowed trackStatus = iota + 1 + trackRedeemed +) + +type trackEntry struct { + status trackStatus + gen uint64 // identifies the current borrow, to detect a redeem racing a re-borrow (ABA) + borrowedAt string + redeemedAt string +} + +type tracker[T any] struct { + mu sync.Mutex + entries map[*T]*trackEntry + nextGen uint64 +} + +func (t *tracker[T]) register() { + t.mu.Lock() + if t.entries == nil { + t.entries = make(map[*T]*trackEntry) + } + t.mu.Unlock() + + registerLeakChecker(t) +} + +// markBorrow records a borrow of ptr and returns its generation. +// +// Caller must hold no lock. +func (t *tracker[T]) markBorrow(ptr *T, site string) uint64 { + t.mu.Lock() + defer t.mu.Unlock() + + e := t.entries[ptr] + if e == nil { + e = &trackEntry{} + t.entries[ptr] = e + } else if e.status == trackBorrowed { + panic(fmt.Sprintf( + "pools: borrow of an object still checked out (borrowed at %s); "+ + "this usually means it was redeemed twice earlier", e.borrowedAt)) + } + + t.nextGen++ + e.status = trackBorrowed + e.gen = t.nextGen + e.borrowedAt = site + + return t.nextGen +} + +// markRedeem validates and records a redeem of ptr. gen is the borrow +// generation the caller is redeeming, or 0 to skip the ABA check (plain +// Pool[T], which has no per-borrow token). +func (t *tracker[T]) markRedeem(ptr *T, gen uint64, site string) { + t.mu.Lock() + defer t.mu.Unlock() + + e := t.entries[ptr] + switch { + case e == nil: + panic("pools: redeem of an object this pool never handed out") + case e.status != trackBorrowed: + panic(fmt.Sprintf("pools: double redeem (first redeemed at %s)", e.redeemedAt)) + case gen != 0 && e.gen != gen: + panic(fmt.Sprintf( + "pools: redeem of a stale borrow (the slot was re-borrowed at %s since this borrow); "+ + "a redeem is racing a re-borrow of the same slot (ABA)", e.borrowedAt)) + } + + e.status = trackRedeemed + e.redeemedAt = site +} + +const stackOffset = 3 + +func (t *tracker[T]) onBorrow(ptr *T) { + t.markBorrow(ptr, caller(stackOffset)) +} + +func (t *tracker[T]) onRedeem(ptr *T) { + t.markRedeem(ptr, 0, caller(stackOffset)) +} + +// borrowRedeemer records the borrow and returns a generation-stamped redeemer +// that validates the redeem (catching double-redeem and ABA) before delegating +// to the cached redeemer. +func (t *tracker[T]) borrowRedeemer(ptr *T, cached func()) func() { + gen := t.markBorrow(ptr, caller(stackOffset)) + + return func() { + t.markRedeem(ptr, gen, caller(stackOffset-1)) + cached() + } +} + +func (t *tracker[T]) checkLeaks(tb TB) bool { + t.mu.Lock() + defer t.mu.Unlock() + + ok := true + for _, e := range t.entries { + if e.status != trackRedeemed { + tb.Logf("pools: object borrowed but never redeemed (borrowed at %s)", e.borrowedAt) + ok = false + } + } + + return ok +} + +func (t *tracker[T]) resetTracking() { + t.mu.Lock() + t.entries = make(map[*T]*trackEntry) + t.nextGen = 0 + t.mu.Unlock() +} + +// leakChecker is the build-erased view of a tracker that the global registry +// holds, so trackers of different element types can be checked uniformly. +type leakChecker interface { + checkLeaks(tb TB) bool + resetTracking() +} + +var ( + registryMu sync.Mutex + registry []leakChecker +) + +func registerLeakChecker(c leakChecker) { + registryMu.Lock() + registry = append(registry, c) + registryMu.Unlock() +} + +// AssertNoLeaks reports whether every borrowed object has been redeemed across +// all pools created so far. +// +// It logs the borrow call site of each leaked object and fails tb when any are +// found. +// +// Typical use, with [ResetTracking] to isolate the test from earlier ones: +// +// func TestX(t *testing.T) { +// pools.ResetTracking() +// t.Cleanup(func() { pools.AssertNoLeaks(t) }) +// // ... exercise code that borrows/redeems ... +// } +func AssertNoLeaks(tb TB) bool { + tb.Helper() + registryMu.Lock() + defer registryMu.Unlock() + + ok := true + for _, c := range registry { + if !c.checkLeaks(tb) { + ok = false + } + } + if !ok { + tb.Errorf("pools: leaked pooled objects detected (borrowed but never redeemed)") + } + + return ok +} + +// ResetTracking clears all recorded borrow/redeem tracking across every pool. +// +// Call it at the start of a test so leaks from earlier tests are not attributed +// to it. +func ResetTracking() { + registryMu.Lock() + defer registryMu.Unlock() + + for _, c := range registry { + c.resetTracking() + } +} + +// caller returns "file:line" of the frame skip levels above caller itself. +func caller(skip int) string { + pc, _, _, ok := runtime.Caller(skip) + if !ok { + return "unknown" + } + fn := runtime.FuncForPC(pc) + if fn == nil { + return "unknown" + } + file, line := fn.FileLine(pc) + + return fmt.Sprintf("%s:%d", file, line) +} diff --git a/vendor/github.com/go-openapi/swag/pools/doc.go b/vendor/github.com/go-openapi/swag/pools/doc.go new file mode 100644 index 000000000..395c24d29 --- /dev/null +++ b/vendor/github.com/go-openapi/swag/pools/doc.go @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +// Package pools provide utilities to recycle allocated objects. +// +// This package provides: +// +// - a generic [Pool] type that wraps [sync.Pool], +// - a [PoolRedeemable] variant that hands out a cached redeem closure, +// - a [PoolSlice] for recycling slices without juggling pointers. +// +// # Debug build +// +// Building with the "poolsdebug" tag (go test -tags poolsdebug ./...) turns on +// instrumentation that tracks every borrow and redeem and panics on misuse: +// +// - double redeem (including the A -> B -> A case for the redeemable pools), +// - redeem of a foreign object, +// - borrow of an object still checked out +// +// It reports the offending call sites. +// +// [AssertNoLeaks] then reports any object borrowed but never redeemed. +// +// The instrumentation is a no-op with zero overhead when the tag is absent. +package pools diff --git a/vendor/github.com/go-openapi/swag/pools/pools.go b/vendor/github.com/go-openapi/swag/pools/pools.go new file mode 100644 index 000000000..d78ea5790 --- /dev/null +++ b/vendor/github.com/go-openapi/swag/pools/pools.go @@ -0,0 +1,410 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package pools + +import ( + "iter" + "slices" + "sync" + "sync/atomic" +) + +// Resettable is an interface for types that want to recycle a clean instance +// from a [Pool]. +// +// When T (or rather *T) implements [Resettable], the pool calls Reset on an +// instance both when it is redeemed and when it is borrowed: +// +// - on redeem, so that no references held by the instance are retained while it sits idle in the +// pool (which would pin a reference graph alive across a GC cycle); +// - on borrow, so that the next borrower receives a clean object regardless of how the instance +// reached the pool. +// +// Reset must be safe to call more than once on the same instance (it runs at +// least twice per cycle). +type Resettable interface { + Reset() +} + +// resetIfResettable calls Reset on v when *T implements [Resettable]. +func resetIfResettable[T any](v *T) { + if r, ok := any(v).(Resettable); ok { + r.Reset() + } +} + +// borrow state of a [redeemable] wrapper, used to detect double-redeem. +const ( + stateIdle uint32 = iota // sitting in the pool (or freshly created), not checked out + stateBorrowed // checked out by a borrower +) + +type redeemable[T any] struct { + inner *T + redeemer func() + // state guards against a double-redeem (the same wrapper Put into the pool + // twice, which would let one object be handed to two borrowers). + // + // It is set to stateBorrowed on borrow and atomically flipped back to + // stateIdle on redeem; a redeem that finds it already idle panics. + state atomic.Uint32 +} + +// redeemPanic is the message raised when a slot is redeemed while already idle. +const redeemPanic = "pools: " + + "double redeem detected (object already returned to the pool); " + + "a borrowed object must be redeemed exactly once" + +// Pool wraps a [sync.Pool] to make it available for any type. +// +// T must be the value type of the pooled object (e.g. Pool[bytes.Buffer]): +// [Pool.Borrow] returns a *T. Using a pointer type as T (e.g. +// Pool[*bytes.Buffer]) would yield a **T and is almost certainly a mistake. +type Pool[T any] struct { + pool sync.Pool + tracker tracker[T] // empty (zero-cost) unless built with the poolsdebug tag +} + +// PoolRedeemable wraps a [sync.Pool] to make it available for any type. +// +// It differs from [Pool] in the way objects are redeemed to the pool: borrowing +// also yields a cached redeem closure, so no closure is allocated at redeem +// time. +type PoolRedeemable[T any] struct { + pool sync.Pool + tracker tracker[redeemable[T]] // empty (zero-cost) unless built with the poolsdebug tag +} + +// New builds a new [Pool] to recycle allocations of type T explicitly using +// [Pool.Redeem] and the allocated pointer. +// +// Freshly allocated instances of type T are set to their zero value; like +// recycled instances they are reset (if [Resettable]) when borrowed, so +// [Pool.Borrow] always yields a clean object. +func New[T any]() *Pool[T] { + p := &Pool[T]{} + p.pool = sync.Pool{ + New: func() any { + return new(T) + }, + } + p.tracker.register() + + return p +} + +// NewRedeemable builds a new redeemable [Pool] to recycle allocations of type +// T, and use the inner redeemer to relinquish objects to the pool. +func NewRedeemable[T any]() *PoolRedeemable[T] { + p := &PoolRedeemable[T]{} + p.pool = sync.Pool{ + New: func() any { + r := &redeemable[T]{inner: new(T)} + r.redeemer = func() { + if !r.state.CompareAndSwap(stateBorrowed, stateIdle) { + panic(redeemPanic) + } + resetIfResettable(r.inner) + p.pool.Put(r) + } + + return r + }, + } + p.tracker.register() + + return p +} + +// Borrow an instance from the pool. +// +// If the type implements [Resettable], the returned instance is reset before +// being handed out, so it is always clean. +func (p *Pool[T]) Borrow() *T { + target := p.pool.Get().(*T) + resetIfResettable(target) + p.tracker.onBorrow(target) + + return target +} + +// Redeem a borrowed instance to the pool. +// +// A nil pointer is ignored (it would otherwise corrupt the pool: a typed-nil +// boxed into an interface is not the nil interface that [sync.Pool.Put] skips). +// +// The instance is reset (if it implements [Resettable]) before being returned +// to the pool. +// After calling Redeem, the caller must drop its reference to ptr: continuing +// to use it is a use-after-redeem bug. +// +// Unlike [PoolRedeemable], this plain pool holds no per-object state, so it +// cannot detect a double-redeem of the same pointer (which corrupts the pool). +// +// Prefer [PoolRedeemable] when you want that guard, or the debug build for full +// tracking. +func (p *Pool[T]) Redeem(ptr *T) { + if ptr == nil { + return + } + p.tracker.onRedeem(ptr) + resetIfResettable(ptr) + p.pool.Put(ptr) +} + +// BorrowWithRedeem borrows an instance from the pool and provides the +// corresponding redeem function. +// +// This is useful for instance to use with defer. +// +// The instance is reset (if it implements [Resettable]) both when borrowed and +// when the returned redeem closure is called. +// After calling the redeem closure, the caller must drop its reference to the +// returned instance. +// +// Calling the redeem closure more than once panics (see [redeemable.state]): a +// borrowed instance must be redeemed exactly once. +func (p *PoolRedeemable[T]) BorrowWithRedeem() (*T, func()) { + container := p.pool.Get().(*redeemable[T]) + container.state.Store(stateBorrowed) + resetIfResettable(container.inner) + + // In release builds borrowRedeemer returns container.redeemer unchanged (zero + // cost). + // Under the poolsdebug tag it returns a generation-stamped wrapper that tracks + // the borrow and detects double-redeem (incl. + // + // ABA), foreign-redeem and leaks. + return container.inner, p.tracker.borrowRedeemer(container, container.redeemer) +} + +// Slice is a struct that wraps a slice []T. +// +// This is useful to borrow and redeem slices from a pool, without having to +// constantly manipulate pointers to the slice. +// +// The wrapper holds the authoritative slice header. +// +// Its mutating methods ([Slice.Append], [Slice.Concat], [Slice.Grow]) return +// the current backing slice for convenience, so it reads as an idiomatic []T. +// +// But the returned slice is only a snapshot of the wrapper's state at that +// moment: if you keep it and grow it yourself with the builtin append and it +// reallocates, the new backing array lives only in your local copy and is NOT +// tracked by the wrapper — it will not be recycled when the wrapper is +// redeemed (and a later borrower would get the old, smaller array). +// +// Rule of thumb: it is fine to read or pass the returned []T to a consumer; but +// if you plan to grow the slice, keep calling the wrapper's methods so the +// growth is tracked and recycled. +type Slice[T any] struct { + length int + inner []T +} + +// Slice returns the inner slice. +// +// Treat the result as a read-only view (for ranging or passing to a consumer), +// valid until the next mutation or redeem. +// To grow or append, use the wrapper methods so the new backing array is +// tracked and recycled (see [Slice]). +func (s *Slice[T]) Slice() []T { + return s.inner +} + +// Grow the inner slice so it can accommodate at least size more elements +// without reallocating, and return the current backing slice. +// +// Growth is tracked by the wrapper, so the enlarged backing array is recycled +// on redeem. +// See [Slice] for the caveat about growing the returned slice yourself. +func (s *Slice[T]) Grow(size int) []T { + s.inner = slices.Grow(s.inner, size) + + return s.inner +} + +func (s *Slice[T]) Len() int { + return len(s.inner) +} + +func (s *Slice[T]) Cap() int { + return cap(s.inner) +} + +// Append elements to the inner slice and return the current backing slice. +// +// This should be preferred to the append builtin if you plan that the slice will +// grow and you want the newly allocated space to be tracked and recycled. +// See [Slice] for the caveat about growing the returned slice yourself. +func (s *Slice[T]) Append(elems ...T) []T { + s.inner = append(s.inner, elems...) + + return s.inner +} + +// Concat another slice to the inner slice and return the current backing slice. +// +// Unlike [slices.Concat], this reuses the inner slice's capacity instead of +// always allocating a fresh backing array. +// See [Slice] for the caveat about growing the returned slice yourself. +func (s *Slice[T]) Concat(slice []T) []T { + s.inner = append(s.inner, slice...) + + return s.inner +} + +// IndexedElems iterates over the inner slice. +func (s *Slice[T]) IndexedElems() iter.Seq2[int, T] { + return func(yield func(int, T) bool) { + for i, elem := range s.inner { + if !yield(i, elem) { + return + } + } + } +} + +// Reset the inner slice to its configured initial length, keeping allocated +// capacity. +// +// All elements are zeroed, so the pool never retains stale element references +// (which would keep a referenced graph alive for slices of pointers) and so a +// [WithLength] slice is handed out clean rather than carrying data from a +// previous borrower. +func (s *Slice[T]) Reset() { + clear(s.inner) + if s.length > cap(s.inner) { + s.inner = slices.Grow(s.inner[:0], s.length) + } + s.inner = s.inner[:s.length] +} + +// Clip removes unused capacity from the inner slice. +func (s *Slice[T]) Clip() { + s.inner = slices.Clip(s.inner) +} + +// resetWithCapacity discards the current backing array and replaces it with a +// fresh one of the configured length and the given capacity. +// +// It is used by a capacity-capped pool to stop recycling an oversized backing +// array (the old array is left for the GC). +func (s *Slice[T]) resetWithCapacity(capacity int) { + s.inner = make([]T, s.length, max(s.length, capacity)) +} + +// PoolSlice is a pool of [Slice[T]]. +// +// [PoolSlice.BorrowWithRedeem] will return an empty inner slice by default. +// This default may be altered using [WithMinimumCapacity]. +// +// Use [PoolSlice.BorrowWithSizeAndRedeem] or [Slice.Grow] to grow the capacity +// of the inner slice. +type PoolSlice[T any] struct { + // redeemable is held as an unexported field rather than embedded, so the + // underlying [PoolRedeemable] and its [sync.Pool] are not part of PoolSlice's + // public surface. + redeemable *PoolRedeemable[Slice[T]] +} + +// PoolSliceOption alters the default settings to allocate new pooled slices +type PoolSliceOption func(*poolSliceOptions) + +type poolSliceOptions struct { + minCapacity int + length int + maxCapacity int +} + +func WithMinimumCapacity(size int) PoolSliceOption { + return func(o *poolSliceOptions) { + o.minCapacity = size + } +} + +// WithMaxCapacity bounds the capacity of recycled slices. +// +// When a borrowed slice has grown past size at redeem time, its (oversized) +// backing array is discarded and replaced with a fresh one sized to the minimum +// capacity, instead of being recycled. +// +// This stops the pool from accumulating large backing arrays after an +// occasional large request, keeping the steady-state memory bounded. +// +// The trade-off: a workload that genuinely needs slices larger than size will +// reallocate on every cycle. +// Set size from the high-water mark you actually expect, not below it. +// A size of 0 (the default) means no cap: grown slices are recycled as-is. +func WithMaxCapacity(size int) PoolSliceOption { + return func(o *poolSliceOptions) { + o.maxCapacity = size + } +} + +// WithLength ensures that the borrowed slices have a fixed given initial +// length. +// +// By default, the borrowed slices are reset to length 0. +func WithLength(size int) PoolSliceOption { + return func(o *poolSliceOptions) { + o.length = size + } +} + +// NewPoolSlice builds a pool to recycle slices of type []T. +func NewPoolSlice[T any](opts ...PoolSliceOption) *PoolSlice[T] { + var o poolSliceOptions + for _, apply := range opts { + apply(&o) + } + + rp := &PoolRedeemable[Slice[T]]{} + rp.pool = sync.Pool{ + New: func() any { + s := &redeemable[Slice[T]]{ + inner: &Slice[T]{ + length: o.length, + inner: make([]T, o.length, max(o.length, o.minCapacity)), + }, + } + + s.redeemer = func() { + if !s.state.CompareAndSwap(stateBorrowed, stateIdle) { + panic(redeemPanic) + } + if o.maxCapacity > 0 && s.inner.Cap() > o.maxCapacity { + s.inner.resetWithCapacity(o.minCapacity) + } else { + s.inner.Reset() + } + rp.pool.Put(s) + } + + return s + }, + } + rp.tracker.register() + + return &PoolSlice[T]{redeemable: rp} +} + +// BorrowWithRedeem returns the slice wrapper and the redeem closure to +// relinquish the allocated wrapper. +// +// The wrapper is reset (elements zeroed, length restored) both on borrow and +// when the redeem closure is called. +// Calling the redeem closure more than once panics. +func (p *PoolSlice[T]) BorrowWithRedeem() (*Slice[T], func()) { + return p.redeemable.BorrowWithRedeem() +} + +// BorrowWithSizeAndRedeem borrows a slice []T from the pool and ensures that +// its capacity is at least the provided size. +func (p *PoolSlice[T]) BorrowWithSizeAndRedeem(size int) (*Slice[T], func()) { + s, redeem := p.BorrowWithRedeem() + s.Grow(size) + + return s, redeem +} diff --git a/vendor/github.com/go-openapi/swag/yamlutils/ordered_map.go b/vendor/github.com/go-openapi/swag/yamlutils/ordered_map.go index 3daf68dbb..24d951f8d 100644 --- a/vendor/github.com/go-openapi/swag/yamlutils/ordered_map.go +++ b/vendor/github.com/go-openapi/swag/yamlutils/ordered_map.go @@ -123,7 +123,7 @@ func (s YAMLMapSlice) MarshalYAML() (any, error) { var nodes []*yaml.Node for _, item := range s { - nn, err := json2yaml(item.Value) + nn, err := json2yaml(item.Value, 1) if err != nil { return nil, err } @@ -153,6 +153,17 @@ func (s YAMLMapSlice) MarshalYAML() (any, error) { // // It implements [yaml.Unmarshaler]. func (s *YAMLMapSlice) UnmarshalYAML(node *yaml.Node) error { + return s.unmarshalYAML(newYAMLWalker(), node, 0) +} + +// unmarshalYAML builds the slice from a [yaml.Node], tracking the recursion depth (against +// stack-overflow) and threading the [yamlWalker] so anchor/alias expansion stays bounded +// across the whole document. +func (s *YAMLMapSlice) unmarshalYAML(w *yamlWalker, node *yaml.Node, depth int) error { + if depth > defaultMaxNestingDepth { + return errMaxNestingDepth + } + if typeutils.IsNil(*s) { // allow to unmarshal with a simple var declaration (nil slice) *s = YAMLMapSlice{} @@ -167,13 +178,17 @@ func (s *YAMLMapSlice) UnmarshalYAML(node *yaml.Node) error { m = m[:0] for i := 0; i < len(node.Content); i += 2 { + if err := w.account(); err != nil { // account the key node + return err + } + var nmi YAMLMapItem k, err := yamlStringScalarC(node.Content[i]) if err != nil { return fmt.Errorf("unable to decode YAML map key: %w: %w", err, ErrYAML) } nmi.Key = k - v, err := yamlNode(node.Content[i+1]) + v, err := w.node(node.Content[i+1], depth+1) if err != nil { return fmt.Errorf("unable to process YAML map value for key %q: %w: %w", k, err, ErrYAML) } @@ -186,7 +201,11 @@ func (s *YAMLMapSlice) UnmarshalYAML(node *yaml.Node) error { return nil } -func json2yaml(item any) (*yaml.Node, error) { +func json2yaml(item any, depth int) (*yaml.Node, error) { + if depth > defaultMaxNestingDepth { + return nil, errMaxNestingDepth + } + if typeutils.IsNil(item) { return &yaml.Node{ Kind: yaml.ScalarNode, @@ -196,7 +215,7 @@ func json2yaml(item any) (*yaml.Node, error) { switch val := item.(type) { case ifaces.Ordered: - return orderedYAML(val) + return orderedYAML(val, depth) case map[string]any: var n yaml.Node @@ -209,7 +228,7 @@ func json2yaml(item any) (*yaml.Node, error) { for _, k := range keys { v := val[k] - childNode, err := json2yaml(v) + childNode, err := json2yaml(v, depth+1) if err != nil { return nil, err } @@ -225,7 +244,7 @@ func json2yaml(item any) (*yaml.Node, error) { var n yaml.Node n.Kind = yaml.SequenceNode for i := range val { - childNode, err := json2yaml(val[i]) + childNode, err := json2yaml(val[i], depth+1) if err != nil { return nil, err } @@ -297,11 +316,11 @@ func uintegerNode[T conv.Unsigned](val T) (*yaml.Node, error) { }, nil } -func orderedYAML[T ifaces.Ordered](val T) (*yaml.Node, error) { +func orderedYAML[T ifaces.Ordered](val T, depth int) (*yaml.Node, error) { var n yaml.Node n.Kind = yaml.MappingNode for key, value := range val.OrderedItems() { - childNode, err := json2yaml(value) + childNode, err := json2yaml(value, depth+1) if err != nil { return nil, err } diff --git a/vendor/github.com/go-openapi/swag/yamlutils/yaml.go b/vendor/github.com/go-openapi/swag/yamlutils/yaml.go index e3aff3c2f..d4b5335f6 100644 --- a/vendor/github.com/go-openapi/swag/yamlutils/yaml.go +++ b/vendor/github.com/go-openapi/swag/yamlutils/yaml.go @@ -12,13 +12,99 @@ import ( yaml "go.yaml.in/yaml/v3" ) +// defaultMaxNestingDepth caps the recursion depth of the YAML<->JSON transforms to +// guard against stack-overflow on deeply nested (possibly adversarial) input. +// +// It matches the limit enforced by go.yaml.in/yaml/v3's own parser and by +// encoding/json's decoder. +const defaultMaxNestingDepth = 10000 + +// Bounds on YAML anchor/alias expansion. +// +// go.yaml.in/yaml/v3 enforces these when decoding into Go values, but that guard is +// coupled to the library's own tree walk: when we decode into a low-level [yaml.Node] +// (to preserve key order) and expand aliases ourselves in [yamlWalker.node], we bypass +// it. We therefore reproduce it here, with the same constants and ratio schedule as the +// library's decoder (see go.yaml.in/yaml/v3 decode.go, "excessive aliasing"). +const ( + aliasCountThreshold = 100 + decodeCountThreshold = 1000 + + // 400,000 decode operations is ~500kb of dense object declarations, or + // ~5kb of dense object declarations with 10000% alias expansion. + aliasRatioRangeLow = 400000 + // 4,000,000 decode operations is ~5MB of dense object declarations. + aliasRatioRangeHigh = 4000000 + aliasRatioRange = float64(aliasRatioRangeHigh - aliasRatioRangeLow) + + // tolerated share of alias-driven decodes: from aliasRatioSmall (small/medium documents) + // down to aliasRatioLarge (very large ones), interpolated with slope aliasRatioSlope. + aliasRatioSmall = 0.99 + aliasRatioLarge = 0.10 + aliasRatioSlope = aliasRatioSmall - aliasRatioLarge +) + +var ( + // errMaxNestingDepth is returned when a document nests deeper than [defaultMaxNestingDepth]. + errMaxNestingDepth = fmt.Errorf("maximum nesting depth of %d exceeded: %w", defaultMaxNestingDepth, ErrYAML) + + // errExcessiveAliasing is returned when anchor/alias expansion is disproportionate to the + // size of the document, i.e. an "alias bomb". + errExcessiveAliasing = fmt.Errorf("document contains excessive aliasing: %w", ErrYAML) +) + +// allowedAliasRatio scales the tolerated share of alias-driven decode operations from 99% +// for small-to-medium documents down to 10% for very large ones, mirroring go.yaml.in/yaml/v3. +func allowedAliasRatio(decodeCount int) float64 { + switch { + case decodeCount <= aliasRatioRangeLow: + return aliasRatioSmall + case decodeCount >= aliasRatioRangeHigh: + return aliasRatioLarge + default: + return aliasRatioSmall - aliasRatioSlope*(float64(decodeCount-aliasRatioRangeLow)/aliasRatioRange) + } +} + +// yamlWalker carries the state needed to bound a single YAML-tree traversal: +// anchor/alias expansion accounting and cycle detection. +// +// A fresh walker is created per top-level conversion; it is threaded (not copied) through +// the whole recursive walk so its counters accumulate across the entire document. +type yamlWalker struct { + decodeCount int + aliasCount int + aliasDepth int + aliases map[*yaml.Node]bool // anchors currently being expanded, for cycle detection +} + +func newYAMLWalker() *yamlWalker { + return &yamlWalker{aliases: make(map[*yaml.Node]bool)} +} + +// account records one processed node and fails if alias expansion has become excessive. +func (w *yamlWalker) account() error { + w.decodeCount++ + if w.aliasDepth > 0 { + w.aliasCount++ + } + + if w.aliasCount > aliasCountThreshold && + w.decodeCount > decodeCountThreshold && + float64(w.aliasCount)/float64(w.decodeCount) > allowedAliasRatio(w.decodeCount) { + return errExcessiveAliasing + } + + return nil +} + // YAMLToJSON converts a YAML document into JSON bytes. // // Note: a YAML document is the output from a [yaml.Marshaler], e.g a pointer to a [yaml.Node]. // // [YAMLToJSON] is typically called after [BytesToYAMLDoc]. func YAMLToJSON(value any) (json.RawMessage, error) { - jm, err := transformData(value) + jm, err := transformData(value, 0) if err != nil { return nil, err } @@ -44,46 +130,73 @@ func BytesToYAMLDoc(data []byte) (any, error) { return &document, nil } -func yamlNode(root *yaml.Node) (any, error) { +func (w *yamlWalker) node(root *yaml.Node, depth int) (any, error) { + if depth > defaultMaxNestingDepth { + return nil, errMaxNestingDepth + } + if err := w.account(); err != nil { + return nil, err + } + switch root.Kind { case yaml.DocumentNode: - return yamlDocument(root) + return w.document(root, depth) case yaml.SequenceNode: - return yamlSequence(root) + return w.sequence(root, depth) case yaml.MappingNode: - return yamlMapping(root) + return w.mapping(root, depth) case yaml.ScalarNode: return yamlScalar(root) case yaml.AliasNode: - return yamlNode(root.Alias) + return w.alias(root, depth) default: return nil, fmt.Errorf("unsupported YAML node type: %v: %w", root.Kind, ErrYAML) } } -func yamlDocument(node *yaml.Node) (any, error) { +// alias resolves an anchor reference, expanding the anchored subtree. It detects cycles +// (an anchor whose expansion transitively references itself) and accounts the expansion +// against the alias-bomb budget via [yamlWalker.aliasDepth]. +func (w *yamlWalker) alias(node *yaml.Node, depth int) (any, error) { + if node.Alias == nil { + return nil, fmt.Errorf("invalid YAML alias node %q: %w", node.Value, ErrYAML) + } + if w.aliases[node.Alias] { + return nil, fmt.Errorf("anchor %q contains itself: %w", node.Value, ErrYAML) + } + + w.aliases[node.Alias] = true + w.aliasDepth++ + out, err := w.node(node.Alias, depth+1) + w.aliasDepth-- + delete(w.aliases, node.Alias) + + return out, err +} + +func (w *yamlWalker) document(node *yaml.Node, depth int) (any, error) { if len(node.Content) != 1 { return nil, fmt.Errorf("unexpected YAML Document node content length: %d: %w", len(node.Content), ErrYAML) } - return yamlNode(node.Content[0]) + return w.node(node.Content[0], depth+1) } -func yamlMapping(node *yaml.Node) (any, error) { +func (w *yamlWalker) mapping(node *yaml.Node, depth int) (any, error) { const sensibleAllocDivider = 2 // nodes concatenate (key,value) sequences m := make(YAMLMapSlice, len(node.Content)/sensibleAllocDivider) - if err := m.UnmarshalYAML(node); err != nil { + if err := m.unmarshalYAML(w, node, depth); err != nil { return nil, err } return m, nil } -func yamlSequence(node *yaml.Node) (any, error) { +func (w *yamlWalker) sequence(node *yaml.Node, depth int) (any, error) { s := make([]any, 0) for i := range len(node.Content) { - v, err := yamlNode(node.Content[i]) + v, err := w.node(node.Content[i], depth+1) if err != nil { return nil, fmt.Errorf("unable to decode YAML sequence value: %w: %w", err, ErrYAML) } @@ -174,12 +287,16 @@ func format(t any) (string, error) { } } -func transformData(input any) (out any, err error) { +func transformData(input any, depth int) (out any, err error) { + if depth > defaultMaxNestingDepth { + return nil, errMaxNestingDepth + } + switch in := input.(type) { case yaml.Node: - return yamlNode(&in) + return newYAMLWalker().node(&in, depth) case *yaml.Node: - return yamlNode(in) + return newYAMLWalker().node(in, depth) case map[any]any: o := make(YAMLMapSlice, 0, len(in)) for ke, va := range in { @@ -188,7 +305,7 @@ func transformData(input any) (out any, err error) { return nil, err } - v, ert := transformData(va) + v, ert := transformData(va, depth+1) if ert != nil { return nil, ert } @@ -200,7 +317,7 @@ func transformData(input any) (out any, err error) { len1 := len(in) o := make([]any, len1) for i := range len1 { - o[i], err = transformData(in[i]) + o[i], err = transformData(in[i], depth+1) if err != nil { return nil, err } diff --git a/vendor/modules.txt b/vendor/modules.txt index 96d0dd081..4ff96bc67 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -105,7 +105,7 @@ github.com/go-openapi/analysis/internal/flatten/sortref # github.com/go-openapi/errors v0.22.8 ## explicit; go 1.25.0 github.com/go-openapi/errors -# github.com/go-openapi/jsonpointer v0.24.0 +# github.com/go-openapi/jsonpointer v1.0.0 ## explicit; go 1.25.0 github.com/go-openapi/jsonpointer github.com/go-openapi/jsonpointer/jsonname @@ -140,43 +140,46 @@ github.com/go-openapi/spec ## explicit; go 1.25.0 github.com/go-openapi/strfmt github.com/go-openapi/strfmt/internal/bsonlite -# github.com/go-openapi/swag v0.27.0 +# github.com/go-openapi/swag v0.27.3 ## explicit; go 1.25.0 github.com/go-openapi/swag -# github.com/go-openapi/swag/cmdutils v0.27.0 +# github.com/go-openapi/swag/cmdutils v0.27.3 ## explicit; go 1.25.0 github.com/go-openapi/swag/cmdutils -# github.com/go-openapi/swag/conv v0.27.0 +# github.com/go-openapi/swag/conv v0.27.3 ## explicit; go 1.25.0 github.com/go-openapi/swag/conv -# github.com/go-openapi/swag/fileutils v0.27.0 +# github.com/go-openapi/swag/fileutils v0.27.3 ## explicit; go 1.25.0 github.com/go-openapi/swag/fileutils # github.com/go-openapi/swag/jsonname v0.27.0 ## explicit; go 1.25.0 github.com/go-openapi/swag/jsonname -# github.com/go-openapi/swag/jsonutils v0.27.0 +# github.com/go-openapi/swag/jsonutils v0.27.3 ## explicit; go 1.25.0 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.27.0 +# github.com/go-openapi/swag/loading v0.27.3 ## explicit; go 1.25.0 github.com/go-openapi/swag/loading -# github.com/go-openapi/swag/mangling v0.27.0 +# github.com/go-openapi/swag/mangling v0.27.3 ## explicit; go 1.25.0 github.com/go-openapi/swag/mangling -# github.com/go-openapi/swag/netutils v0.27.0 +# github.com/go-openapi/swag/netutils v0.27.3 ## explicit; go 1.25.0 github.com/go-openapi/swag/netutils -# github.com/go-openapi/swag/stringutils v0.27.0 +# github.com/go-openapi/swag/pools v0.27.3 +## explicit; go 1.25.0 +github.com/go-openapi/swag/pools +# github.com/go-openapi/swag/stringutils v0.27.3 ## explicit; go 1.25.0 github.com/go-openapi/swag/stringutils -# github.com/go-openapi/swag/typeutils v0.27.0 +# github.com/go-openapi/swag/typeutils v0.27.3 ## explicit; go 1.25.0 github.com/go-openapi/swag/typeutils -# github.com/go-openapi/swag/yamlutils v0.27.0 +# github.com/go-openapi/swag/yamlutils v0.27.3 ## explicit; go 1.25.0 github.com/go-openapi/swag/yamlutils # github.com/go-openapi/validate v0.26.0