From ae1a98558acf1ff74979954d8279f45a8ba3593a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 10:45:54 +0000 Subject: [PATCH 01/26] [chore]: Bump github.com/tdewolff/minify/v2 from 2.21.1 to 2.21.2 (#3567) Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.21.1 to 2.21.2. - [Release notes](https://github.com/tdewolff/minify/releases) - [Commits](https://github.com/tdewolff/minify/compare/v2.21.1...v2.21.2) --- updated-dependencies: - dependency-name: github.com/tdewolff/minify/v2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- vendor/github.com/tdewolff/parse/v2/buffer/lexer.go | 5 ++--- vendor/github.com/tdewolff/parse/v2/common.go | 10 +++++++--- vendor/github.com/tdewolff/parse/v2/html/lex.go | 3 ++- vendor/github.com/tdewolff/parse/v2/input.go | 5 ++--- vendor/modules.txt | 4 ++-- 7 files changed, 21 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 9e6db71f1..0238adc52 100644 --- a/go.mod +++ b/go.mod @@ -68,7 +68,7 @@ require ( github.com/superseriousbusiness/activity v1.9.0-gts github.com/superseriousbusiness/httpsig v1.2.0-SSB github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8 - github.com/tdewolff/minify/v2 v2.21.1 + github.com/tdewolff/minify/v2 v2.21.2 github.com/technologize/otel-go-contrib v1.1.1 github.com/tetratelabs/wazero v1.8.1 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 @@ -213,7 +213,7 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe // indirect github.com/superseriousbusiness/go-png-image-structure/v2 v2.0.1-SSB // indirect - github.com/tdewolff/parse/v2 v2.7.18 // indirect + github.com/tdewolff/parse/v2 v2.7.19 // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect github.com/toqueteos/webbrowser v1.2.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect diff --git a/go.sum b/go.sum index f00a34326..d4835c21a 100644 --- a/go.sum +++ b/go.sum @@ -539,10 +539,10 @@ github.com/superseriousbusiness/httpsig v1.2.0-SSB h1:BinBGKbf2LSuVT5+MuH0XynHN9 github.com/superseriousbusiness/httpsig v1.2.0-SSB/go.mod h1:+rxfATjFaDoDIVaJOTSP0gj6UrbicaYPEptvCLC9F28= github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8 h1:nTIhuP157oOFcscuoK1kCme1xTeGIzztSw70lX9NrDQ= github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8/go.mod h1:uYC/W92oVRJ49Vh1GcvTqpeFqHi+Ovrl2sMllQWRAEo= -github.com/tdewolff/minify/v2 v2.21.1 h1:AAf5iltw6+KlUvjRNPAPrANIXl3XEJNBBzuZom5iCAM= -github.com/tdewolff/minify/v2 v2.21.1/go.mod h1:PoqFH8ugcuTUvKqVM9vOqXw4msxvuhL/DTmV5ZXhSCI= -github.com/tdewolff/parse/v2 v2.7.18 h1:uSqjEMT2lwCj5oifBHDcWU2kN1pbLrRENgFWDJa57eI= -github.com/tdewolff/parse/v2 v2.7.18/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= +github.com/tdewolff/minify/v2 v2.21.2 h1:VfTvmGVtBYhMTlUAeHtXM7XOsW0JT/6uMwUPPqgUs9k= +github.com/tdewolff/minify/v2 v2.21.2/go.mod h1:Olje3eHdBnrMjINKffDsil/3NV98Iv7MhWf7556WQVg= +github.com/tdewolff/parse/v2 v2.7.19 h1:7Ljh26yj+gdLFEq/7q9LT4SYyKtwQX4ocNrj45UCePg= +github.com/tdewolff/parse/v2 v2.7.19/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo= github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= diff --git a/vendor/github.com/tdewolff/parse/v2/buffer/lexer.go b/vendor/github.com/tdewolff/parse/v2/buffer/lexer.go index 46e6bdafd..3c9da22d3 100644 --- a/vendor/github.com/tdewolff/parse/v2/buffer/lexer.go +++ b/vendor/github.com/tdewolff/parse/v2/buffer/lexer.go @@ -2,7 +2,6 @@ package buffer import ( "io" - "io/ioutil" ) var nullBuffer = []byte{0} @@ -18,7 +17,7 @@ type Lexer struct { restore func() } -// NewLexer returns a new Lexer for a given io.Reader, and uses ioutil.ReadAll to read it into a byte slice. +// NewLexer returns a new Lexer for a given io.Reader, and uses io.ReadAll to read it into a byte slice. // If the io.Reader implements Bytes, that is used instead. // It will append a NULL at the end of the buffer. func NewLexer(r io.Reader) *Lexer { @@ -30,7 +29,7 @@ func NewLexer(r io.Reader) *Lexer { b = buffer.Bytes() } else { var err error - b, err = ioutil.ReadAll(r) + b, err = io.ReadAll(r) if err != nil { return &Lexer{ buf: nullBuffer, diff --git a/vendor/github.com/tdewolff/parse/v2/common.go b/vendor/github.com/tdewolff/parse/v2/common.go index e0795304c..1883d1bd4 100644 --- a/vendor/github.com/tdewolff/parse/v2/common.go +++ b/vendor/github.com/tdewolff/parse/v2/common.go @@ -317,9 +317,13 @@ func replaceEntities(b []byte, i int, entitiesMap map[string][]byte, revEntities } } else { for ; j < len(b) && j-i-1 <= MaxEntityLength && b[j] != ';'; j++ { + if !(b[j] >= '0' && b[j] <= '9' || b[j] >= 'a' && b[j] <= 'z' || b[j] >= 'A' && b[j] <= 'Z') { + // invalid character reference character + break + } } - if j <= i+1 || len(b) <= j { - return b, j - 1 + if len(b) <= j || j == i+1 || b[j] != ';' { + return b, i } var ok bool @@ -399,7 +403,7 @@ func ReplaceMultipleWhitespaceAndEntities(b []byte, entitiesMap map[string][]byt if j == 0 { return b } else if j == 1 { // only if starts with whitespace - b[k-1] = b[0] + b[k-1] = b[0] // move newline to end of whitespace return b[k-1:] } else if k < len(b) { j += copy(b[j:], b[k:]) diff --git a/vendor/github.com/tdewolff/parse/v2/html/lex.go b/vendor/github.com/tdewolff/parse/v2/html/lex.go index b24d4dcd2..8774ea264 100644 --- a/vendor/github.com/tdewolff/parse/v2/html/lex.go +++ b/vendor/github.com/tdewolff/parse/v2/html/lex.go @@ -362,7 +362,8 @@ func (l *Lexer) shiftBogusComment() []byte { func (l *Lexer) shiftStartTag() (TokenType, []byte) { for { - if c := l.r.Peek(0); (c < 'a' || 'z' < c) && (c < 'A' || 'Z' < c) && (c < '0' || '9' < c) && c != '-' { + // spec says only a-zA-Z0-9, but we're lenient here + if c := l.r.Peek(0); c == ' ' || c == '>' || c == '/' && l.r.Peek(1) == '>' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == 0 && l.r.Err() != nil || 0 < len(l.tmplBegin) && l.at(l.tmplBegin...) { break } l.r.Move(1) diff --git a/vendor/github.com/tdewolff/parse/v2/input.go b/vendor/github.com/tdewolff/parse/v2/input.go index 924f14f0c..586ad7306 100644 --- a/vendor/github.com/tdewolff/parse/v2/input.go +++ b/vendor/github.com/tdewolff/parse/v2/input.go @@ -2,7 +2,6 @@ package parse import ( "io" - "io/ioutil" ) var nullBuffer = []byte{0} @@ -18,7 +17,7 @@ type Input struct { restore func() } -// NewInput returns a new Input for a given io.Input and uses ioutil.ReadAll to read it into a byte slice. +// NewInput returns a new Input for a given io.Input and uses io.ReadAll to read it into a byte slice. // If the io.Input implements Bytes, that is used instead. It will append a NULL at the end of the buffer. func NewInput(r io.Reader) *Input { var b []byte @@ -29,7 +28,7 @@ func NewInput(r io.Reader) *Input { b = buffer.Bytes() } else { var err error - b, err = ioutil.ReadAll(r) + b, err = io.ReadAll(r) if err != nil { return &Input{ buf: nullBuffer, diff --git a/vendor/modules.txt b/vendor/modules.txt index a5465ec96..68fb93086 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -832,11 +832,11 @@ github.com/superseriousbusiness/oauth2/v4/generates github.com/superseriousbusiness/oauth2/v4/manage github.com/superseriousbusiness/oauth2/v4/models github.com/superseriousbusiness/oauth2/v4/server -# github.com/tdewolff/minify/v2 v2.21.1 +# github.com/tdewolff/minify/v2 v2.21.2 ## explicit; go 1.18 github.com/tdewolff/minify/v2 github.com/tdewolff/minify/v2/html -# github.com/tdewolff/parse/v2 v2.7.18 +# github.com/tdewolff/parse/v2 v2.7.19 ## explicit; go 1.13 github.com/tdewolff/parse/v2 github.com/tdewolff/parse/v2/buffer From 2ed409888b988115433b5eddb62ca38c00c68325 Mon Sep 17 00:00:00 2001 From: Daenney Date: Mon, 25 Nov 2024 11:50:03 +0100 Subject: [PATCH 02/26] [chore] Update gorilla/websocket (#3561) The maintainers messed with the v1.5.2 tag which causes Go checksum validation problems as the Go module proxy saw and recorded the original hash. This updates to 1.5.3 which doesn't have the issue. --- go.mod | 2 +- go.sum | 2 + .../gorilla/websocket/.editorconfig | 20 -- .../github.com/gorilla/websocket/.gitignore | 26 +- .../gorilla/websocket/.golangci.yml | 13 - vendor/github.com/gorilla/websocket/AUTHORS | 9 + vendor/github.com/gorilla/websocket/LICENSE | 41 +-- vendor/github.com/gorilla/websocket/Makefile | 34 -- vendor/github.com/gorilla/websocket/README.md | 25 +- vendor/github.com/gorilla/websocket/client.go | 13 +- vendor/github.com/gorilla/websocket/conn.go | 72 ++-- vendor/github.com/gorilla/websocket/proxy.go | 6 +- vendor/github.com/gorilla/websocket/server.go | 12 +- .../gorilla/websocket/tls_handshake.go | 3 + .../golang.org/x/net/internal/socks/client.go | 168 ---------- .../golang.org/x/net/internal/socks/socks.go | 317 ------------------ vendor/golang.org/x/net/proxy/dial.go | 54 --- vendor/golang.org/x/net/proxy/direct.go | 31 -- vendor/golang.org/x/net/proxy/per_host.go | 151 --------- vendor/golang.org/x/net/proxy/proxy.go | 149 -------- vendor/golang.org/x/net/proxy/socks5.go | 42 --- vendor/modules.txt | 6 +- 22 files changed, 115 insertions(+), 1081 deletions(-) delete mode 100644 vendor/github.com/gorilla/websocket/.editorconfig delete mode 100644 vendor/github.com/gorilla/websocket/.golangci.yml create mode 100644 vendor/github.com/gorilla/websocket/AUTHORS delete mode 100644 vendor/github.com/gorilla/websocket/Makefile delete mode 100644 vendor/golang.org/x/net/internal/socks/client.go delete mode 100644 vendor/golang.org/x/net/internal/socks/socks.go delete mode 100644 vendor/golang.org/x/net/proxy/dial.go delete mode 100644 vendor/golang.org/x/net/proxy/direct.go delete mode 100644 vendor/golang.org/x/net/proxy/per_host.go delete mode 100644 vendor/golang.org/x/net/proxy/proxy.go delete mode 100644 vendor/golang.org/x/net/proxy/socks5.go diff --git a/go.mod b/go.mod index 0238adc52..261aefba4 100644 --- a/go.mod +++ b/go.mod @@ -52,7 +52,7 @@ require ( github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 github.com/gorilla/feeds v1.2.0 - github.com/gorilla/websocket v1.5.2 + github.com/gorilla/websocket v1.5.3 github.com/jackc/pgx/v5 v5.7.1 github.com/k3a/html2text v1.2.1 github.com/microcosm-cc/bluemonday v1.0.27 diff --git a/go.sum b/go.sum index d4835c21a..a59ca6946 100644 --- a/go.sum +++ b/go.sum @@ -332,6 +332,8 @@ github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8L github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.2 h1:qoW6V1GT3aZxybsbC6oLnailWnB+qTMVwMreOso9XUw= github.com/gorilla/websocket v1.5.2/go.mod h1:0n9H61RBAcf5/38py2MCYbxzPIY9rOkpvvMT24Rqs30= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= diff --git a/vendor/github.com/gorilla/websocket/.editorconfig b/vendor/github.com/gorilla/websocket/.editorconfig deleted file mode 100644 index 2940ec92a..000000000 --- a/vendor/github.com/gorilla/websocket/.editorconfig +++ /dev/null @@ -1,20 +0,0 @@ -; https://editorconfig.org/ - -root = true - -[*] -insert_final_newline = true -charset = utf-8 -trim_trailing_whitespace = true -indent_style = space -indent_size = 2 - -[{Makefile,go.mod,go.sum,*.go,.gitmodules}] -indent_style = tab -indent_size = 4 - -[*.md] -indent_size = 4 -trim_trailing_whitespace = false - -eclint_indent_style = unset diff --git a/vendor/github.com/gorilla/websocket/.gitignore b/vendor/github.com/gorilla/websocket/.gitignore index 84039fec6..cd3fcd1ef 100644 --- a/vendor/github.com/gorilla/websocket/.gitignore +++ b/vendor/github.com/gorilla/websocket/.gitignore @@ -1 +1,25 @@ -coverage.coverprofile +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe + +.idea/ +*.iml diff --git a/vendor/github.com/gorilla/websocket/.golangci.yml b/vendor/github.com/gorilla/websocket/.golangci.yml deleted file mode 100644 index 44cf86a06..000000000 --- a/vendor/github.com/gorilla/websocket/.golangci.yml +++ /dev/null @@ -1,13 +0,0 @@ -run: - timeout: "5m" - # will not run golangci-lint against *_test.go - tests: false -issues: - exclude-dirs: - - examples/*.go - exclude-rules: - # excluding error checks from all the .go files - - path: ./*.go - linters: - - errcheck - diff --git a/vendor/github.com/gorilla/websocket/AUTHORS b/vendor/github.com/gorilla/websocket/AUTHORS new file mode 100644 index 000000000..1931f4006 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/AUTHORS @@ -0,0 +1,9 @@ +# This is the official list of Gorilla WebSocket authors for copyright +# purposes. +# +# Please keep the list sorted. + +Gary Burd +Google LLC (https://opensource.google.com/) +Joachim Bauch + diff --git a/vendor/github.com/gorilla/websocket/LICENSE b/vendor/github.com/gorilla/websocket/LICENSE index 8692af650..9171c9722 100644 --- a/vendor/github.com/gorilla/websocket/LICENSE +++ b/vendor/github.com/gorilla/websocket/LICENSE @@ -1,27 +1,22 @@ -Copyright (c) 2023 The Gorilla Authors. All rights reserved. +Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved. Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: +modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/gorilla/websocket/Makefile b/vendor/github.com/gorilla/websocket/Makefile deleted file mode 100644 index 603a63f50..000000000 --- a/vendor/github.com/gorilla/websocket/Makefile +++ /dev/null @@ -1,34 +0,0 @@ -GO_LINT=$(shell which golangci-lint 2> /dev/null || echo '') -GO_LINT_URI=github.com/golangci/golangci-lint/cmd/golangci-lint@latest - -GO_SEC=$(shell which gosec 2> /dev/null || echo '') -GO_SEC_URI=github.com/securego/gosec/v2/cmd/gosec@latest - -GO_VULNCHECK=$(shell which govulncheck 2> /dev/null || echo '') -GO_VULNCHECK_URI=golang.org/x/vuln/cmd/govulncheck@latest - -.PHONY: golangci-lint -golangci-lint: - $(if $(GO_LINT), ,go install $(GO_LINT_URI)) - @echo "##### Running golangci-lint" - golangci-lint run -v - -.PHONY: gosec -gosec: - $(if $(GO_SEC), ,go install $(GO_SEC_URI)) - @echo "##### Running gosec" - gosec -exclude-dir examples ./... - -.PHONY: govulncheck -govulncheck: - $(if $(GO_VULNCHECK), ,go install $(GO_VULNCHECK_URI)) - @echo "##### Running govulncheck" - govulncheck ./... - -.PHONY: verify -verify: golangci-lint gosec govulncheck - -.PHONY: test -test: - @echo "##### Running tests" - go test -race -cover -coverprofile=coverage.coverprofile -covermode=atomic -v ./... diff --git a/vendor/github.com/gorilla/websocket/README.md b/vendor/github.com/gorilla/websocket/README.md index 525a62a92..d33ed7fdd 100644 --- a/vendor/github.com/gorilla/websocket/README.md +++ b/vendor/github.com/gorilla/websocket/README.md @@ -1,23 +1,19 @@ -# gorilla/websocket +# Gorilla WebSocket -![testing](https://github.com/gorilla/websocket/actions/workflows/test.yml/badge.svg) -[![codecov](https://codecov.io/github/gorilla/websocket/branch/main/graph/badge.svg)](https://codecov.io/github/gorilla/websocket) -[![godoc](https://godoc.org/github.com/gorilla/websocket?status.svg)](https://godoc.org/github.com/gorilla/websocket) -[![sourcegraph](https://sourcegraph.com/github.com/gorilla/websocket/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/websocket?badge) +[![GoDoc](https://godoc.org/github.com/gorilla/websocket?status.svg)](https://godoc.org/github.com/gorilla/websocket) +[![CircleCI](https://circleci.com/gh/gorilla/websocket.svg?style=svg)](https://circleci.com/gh/gorilla/websocket) -Gorilla WebSocket is a [Go](http://golang.org/) implementation of the [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. - -![Gorilla Logo](https://github.com/gorilla/.github/assets/53367916/d92caabf-98e0-473e-bfbf-ab554ba435e5) +Gorilla WebSocket is a [Go](http://golang.org/) implementation of the +[WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. ### Documentation * [API Reference](https://pkg.go.dev/github.com/gorilla/websocket?tab=doc) -* [Chat example](https://github.com/gorilla/websocket/tree/main/examples/chat) -* [Command example](https://github.com/gorilla/websocket/tree/main/examples/command) -* [Client and server example](https://github.com/gorilla/websocket/tree/main/examples/echo) -* [File watch example](https://github.com/gorilla/websocket/tree/main/examples/filewatch) -* [Write buffer pool example](https://github.com/gorilla/websocket/tree/main/examples/bufferpool) +* [Chat example](https://github.com/gorilla/websocket/tree/master/examples/chat) +* [Command example](https://github.com/gorilla/websocket/tree/master/examples/command) +* [Client and server example](https://github.com/gorilla/websocket/tree/master/examples/echo) +* [File watch example](https://github.com/gorilla/websocket/tree/master/examples/filewatch) ### Status @@ -33,4 +29,5 @@ package API is stable. The Gorilla WebSocket package passes the server tests in the [Autobahn Test Suite](https://github.com/crossbario/autobahn-testsuite) using the application in the [examples/autobahn -subdirectory](https://github.com/gorilla/websocket/tree/main/examples/autobahn). +subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn). + diff --git a/vendor/github.com/gorilla/websocket/client.go b/vendor/github.com/gorilla/websocket/client.go index 7023e1176..04fdafee1 100644 --- a/vendor/github.com/gorilla/websocket/client.go +++ b/vendor/github.com/gorilla/websocket/client.go @@ -11,14 +11,13 @@ import ( "errors" "fmt" "io" + "io/ioutil" "net" "net/http" "net/http/httptrace" "net/url" "strings" "time" - - "golang.org/x/net/proxy" ) // ErrBadHandshake is returned when the server response to opening handshake is @@ -305,7 +304,7 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h return nil, nil, err } if proxyURL != nil { - dialer, err := proxy.FromURL(proxyURL, netDialerFunc(netDial)) + dialer, err := proxy_FromURL(proxyURL, netDialerFunc(netDial)) if err != nil { return nil, nil, err } @@ -392,7 +391,7 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h } } - if resp.StatusCode != http.StatusSwitchingProtocols || + if resp.StatusCode != 101 || !tokenListContainsValue(resp.Header, "Upgrade", "websocket") || !tokenListContainsValue(resp.Header, "Connection", "upgrade") || resp.Header.Get("Sec-Websocket-Accept") != computeAcceptKey(challengeKey) { @@ -401,7 +400,7 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h // debugging. buf := make([]byte, 1024) n, _ := io.ReadFull(resp.Body, buf) - resp.Body = io.NopCloser(bytes.NewReader(buf[:n])) + resp.Body = ioutil.NopCloser(bytes.NewReader(buf[:n])) return nil, resp, ErrBadHandshake } @@ -419,7 +418,7 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h break } - resp.Body = io.NopCloser(bytes.NewReader([]byte{})) + resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{})) conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol") netConn.SetDeadline(time.Time{}) @@ -429,7 +428,7 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h func cloneTLSConfig(cfg *tls.Config) *tls.Config { if cfg == nil { - return &tls.Config{MinVersion: tls.VersionTLS12} + return &tls.Config{} } return cfg.Clone() } diff --git a/vendor/github.com/gorilla/websocket/conn.go b/vendor/github.com/gorilla/websocket/conn.go index 49399b120..5161ef81f 100644 --- a/vendor/github.com/gorilla/websocket/conn.go +++ b/vendor/github.com/gorilla/websocket/conn.go @@ -6,10 +6,11 @@ package websocket import ( "bufio" - "crypto/rand" "encoding/binary" "errors" "io" + "io/ioutil" + "math/rand" "net" "strconv" "strings" @@ -180,16 +181,16 @@ var ( errInvalidControlFrame = errors.New("websocket: invalid control frame") ) -// maskRand is an io.Reader for generating mask bytes. The reader is initialized -// to crypto/rand Reader. Tests swap the reader to a math/rand reader for -// reproducible results. -var maskRand = rand.Reader - -// newMaskKey returns a new 32 bit value for masking client frames. func newMaskKey() [4]byte { - var k [4]byte - _, _ = io.ReadFull(maskRand, k[:]) - return k + n := rand.Uint32() + return [4]byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)} +} + +func hideTempErr(err error) error { + if e, ok := err.(net.Error); ok && e.Temporary() { + err = &netError{msg: e.Error(), timeout: e.Timeout()} + } + return err } func isControl(frameType int) bool { @@ -357,6 +358,7 @@ func (c *Conn) RemoteAddr() net.Addr { // Write methods func (c *Conn) writeFatal(err error) error { + err = hideTempErr(err) c.writeErrMu.Lock() if c.writeErr == nil { c.writeErr = err @@ -434,27 +436,21 @@ func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) er maskBytes(key, 0, buf[6:]) } - if deadline.IsZero() { - // No timeout for zero time. - <-c.mu - } else { - d := time.Until(deadline) + d := 1000 * time.Hour + if !deadline.IsZero() { + d = deadline.Sub(time.Now()) if d < 0 { return errWriteTimeout } - select { - case <-c.mu: - default: - timer := time.NewTimer(d) - select { - case <-c.mu: - timer.Stop() - case <-timer.C: - return errWriteTimeout - } - } } + timer := time.NewTimer(d) + select { + case <-c.mu: + timer.Stop() + case <-timer.C: + return errWriteTimeout + } defer func() { c.mu <- struct{}{} }() c.writeErrMu.Lock() @@ -799,7 +795,7 @@ func (c *Conn) advanceFrame() (int, error) { // 1. Skip remainder of previous frame. if c.readRemaining > 0 { - if _, err := io.CopyN(io.Discard, c.br, c.readRemaining); err != nil { + if _, err := io.CopyN(ioutil.Discard, c.br, c.readRemaining); err != nil { return noFrame, err } } @@ -1012,7 +1008,7 @@ func (c *Conn) NextReader() (messageType int, r io.Reader, err error) { for c.readErr == nil { frameType, err := c.advanceFrame() if err != nil { - c.readErr = err + c.readErr = hideTempErr(err) break } @@ -1052,7 +1048,7 @@ func (r *messageReader) Read(b []byte) (int, error) { b = b[:c.readRemaining] } n, err := c.br.Read(b) - c.readErr = err + c.readErr = hideTempErr(err) if c.isServer { c.readMaskPos = maskBytes(c.readMaskKey, c.readMaskPos, b[:n]) } @@ -1073,7 +1069,7 @@ func (r *messageReader) Read(b []byte) (int, error) { frameType, err := c.advanceFrame() switch { case err != nil: - c.readErr = err + c.readErr = hideTempErr(err) case frameType == TextMessage || frameType == BinaryMessage: c.readErr = errors.New("websocket: internal error, unexpected text or binary in Reader") } @@ -1098,7 +1094,7 @@ func (c *Conn) ReadMessage() (messageType int, p []byte, err error) { if err != nil { return messageType, nil, err } - p, err = io.ReadAll(r) + p, err = ioutil.ReadAll(r) return messageType, p, err } @@ -1165,7 +1161,7 @@ func (c *Conn) SetPingHandler(h func(appData string) error) { err := c.WriteControl(PongMessage, []byte(message), time.Now().Add(writeWait)) if err == ErrCloseSent { return nil - } else if _, ok := err.(net.Error); ok { + } else if e, ok := err.(net.Error); ok && e.Temporary() { return nil } return err @@ -1240,15 +1236,3 @@ func FormatCloseMessage(closeCode int, text string) []byte { copy(buf[2:], text) return buf } - -var messageTypes = map[int]string{ - TextMessage: "TextMessage", - BinaryMessage: "BinaryMessage", - CloseMessage: "CloseMessage", - PingMessage: "PingMessage", - PongMessage: "PongMessage", -} - -func FormatMessageType(mt int) string { - return messageTypes[mt] -} diff --git a/vendor/github.com/gorilla/websocket/proxy.go b/vendor/github.com/gorilla/websocket/proxy.go index 3c570c26f..e0f466b72 100644 --- a/vendor/github.com/gorilla/websocket/proxy.go +++ b/vendor/github.com/gorilla/websocket/proxy.go @@ -12,8 +12,6 @@ import ( "net/http" "net/url" "strings" - - "golang.org/x/net/proxy" ) type netDialerFunc func(network, addr string) (net.Conn, error) @@ -23,7 +21,7 @@ func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) { } func init() { - proxy.RegisterDialerType("http", func(proxyURL *url.URL, forwardDialer proxy.Dialer) (proxy.Dialer, error) { + proxy_RegisterDialerType("http", func(proxyURL *url.URL, forwardDialer proxy_Dialer) (proxy_Dialer, error) { return &httpProxyDialer{proxyURL: proxyURL, forwardDial: forwardDialer.Dial}, nil }) } @@ -70,7 +68,7 @@ func (hpd *httpProxyDialer) Dial(network string, addr string) (net.Conn, error) return nil, err } - if resp.StatusCode != http.StatusOK { + if resp.StatusCode != 200 { conn.Close() f := strings.SplitN(resp.Status, " ", 2) return nil, errors.New(f[1]) diff --git a/vendor/github.com/gorilla/websocket/server.go b/vendor/github.com/gorilla/websocket/server.go index fda75ff0d..bb3359743 100644 --- a/vendor/github.com/gorilla/websocket/server.go +++ b/vendor/github.com/gorilla/websocket/server.go @@ -33,7 +33,6 @@ type Upgrader struct { // size is zero, then buffers allocated by the HTTP server are used. The // I/O buffer sizes do not limit the size of the messages that can be sent // or received. - // The default value is 4096 bytes, 4kb. ReadBufferSize, WriteBufferSize int // WriteBufferPool is a pool of buffers for write operations. If the value @@ -102,8 +101,8 @@ func checkSameOrigin(r *http.Request) bool { func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header) string { if u.Subprotocols != nil { clientProtocols := Subprotocols(r) - for _, clientProtocol := range clientProtocols { - for _, serverProtocol := range u.Subprotocols { + for _, serverProtocol := range u.Subprotocols { + for _, clientProtocol := range clientProtocols { if clientProtocol == serverProtocol { return clientProtocol } @@ -173,7 +172,12 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade } } - netConn, brw, err := http.NewResponseController(w).Hijack() + h, ok := w.(http.Hijacker) + if !ok { + return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker") + } + var brw *bufio.ReadWriter + netConn, brw, err := h.Hijack() if err != nil { return u.returnError(w, r, http.StatusInternalServerError, err.Error()) } diff --git a/vendor/github.com/gorilla/websocket/tls_handshake.go b/vendor/github.com/gorilla/websocket/tls_handshake.go index 7f3864534..a62b68ccb 100644 --- a/vendor/github.com/gorilla/websocket/tls_handshake.go +++ b/vendor/github.com/gorilla/websocket/tls_handshake.go @@ -1,3 +1,6 @@ +//go:build go1.17 +// +build go1.17 + package websocket import ( diff --git a/vendor/golang.org/x/net/internal/socks/client.go b/vendor/golang.org/x/net/internal/socks/client.go deleted file mode 100644 index 3d6f516a5..000000000 --- a/vendor/golang.org/x/net/internal/socks/client.go +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package socks - -import ( - "context" - "errors" - "io" - "net" - "strconv" - "time" -) - -var ( - noDeadline = time.Time{} - aLongTimeAgo = time.Unix(1, 0) -) - -func (d *Dialer) connect(ctx context.Context, c net.Conn, address string) (_ net.Addr, ctxErr error) { - host, port, err := splitHostPort(address) - if err != nil { - return nil, err - } - if deadline, ok := ctx.Deadline(); ok && !deadline.IsZero() { - c.SetDeadline(deadline) - defer c.SetDeadline(noDeadline) - } - if ctx != context.Background() { - errCh := make(chan error, 1) - done := make(chan struct{}) - defer func() { - close(done) - if ctxErr == nil { - ctxErr = <-errCh - } - }() - go func() { - select { - case <-ctx.Done(): - c.SetDeadline(aLongTimeAgo) - errCh <- ctx.Err() - case <-done: - errCh <- nil - } - }() - } - - b := make([]byte, 0, 6+len(host)) // the size here is just an estimate - b = append(b, Version5) - if len(d.AuthMethods) == 0 || d.Authenticate == nil { - b = append(b, 1, byte(AuthMethodNotRequired)) - } else { - ams := d.AuthMethods - if len(ams) > 255 { - return nil, errors.New("too many authentication methods") - } - b = append(b, byte(len(ams))) - for _, am := range ams { - b = append(b, byte(am)) - } - } - if _, ctxErr = c.Write(b); ctxErr != nil { - return - } - - if _, ctxErr = io.ReadFull(c, b[:2]); ctxErr != nil { - return - } - if b[0] != Version5 { - return nil, errors.New("unexpected protocol version " + strconv.Itoa(int(b[0]))) - } - am := AuthMethod(b[1]) - if am == AuthMethodNoAcceptableMethods { - return nil, errors.New("no acceptable authentication methods") - } - if d.Authenticate != nil { - if ctxErr = d.Authenticate(ctx, c, am); ctxErr != nil { - return - } - } - - b = b[:0] - b = append(b, Version5, byte(d.cmd), 0) - if ip := net.ParseIP(host); ip != nil { - if ip4 := ip.To4(); ip4 != nil { - b = append(b, AddrTypeIPv4) - b = append(b, ip4...) - } else if ip6 := ip.To16(); ip6 != nil { - b = append(b, AddrTypeIPv6) - b = append(b, ip6...) - } else { - return nil, errors.New("unknown address type") - } - } else { - if len(host) > 255 { - return nil, errors.New("FQDN too long") - } - b = append(b, AddrTypeFQDN) - b = append(b, byte(len(host))) - b = append(b, host...) - } - b = append(b, byte(port>>8), byte(port)) - if _, ctxErr = c.Write(b); ctxErr != nil { - return - } - - if _, ctxErr = io.ReadFull(c, b[:4]); ctxErr != nil { - return - } - if b[0] != Version5 { - return nil, errors.New("unexpected protocol version " + strconv.Itoa(int(b[0]))) - } - if cmdErr := Reply(b[1]); cmdErr != StatusSucceeded { - return nil, errors.New("unknown error " + cmdErr.String()) - } - if b[2] != 0 { - return nil, errors.New("non-zero reserved field") - } - l := 2 - var a Addr - switch b[3] { - case AddrTypeIPv4: - l += net.IPv4len - a.IP = make(net.IP, net.IPv4len) - case AddrTypeIPv6: - l += net.IPv6len - a.IP = make(net.IP, net.IPv6len) - case AddrTypeFQDN: - if _, err := io.ReadFull(c, b[:1]); err != nil { - return nil, err - } - l += int(b[0]) - default: - return nil, errors.New("unknown address type " + strconv.Itoa(int(b[3]))) - } - if cap(b) < l { - b = make([]byte, l) - } else { - b = b[:l] - } - if _, ctxErr = io.ReadFull(c, b); ctxErr != nil { - return - } - if a.IP != nil { - copy(a.IP, b) - } else { - a.Name = string(b[:len(b)-2]) - } - a.Port = int(b[len(b)-2])<<8 | int(b[len(b)-1]) - return &a, nil -} - -func splitHostPort(address string) (string, int, error) { - host, port, err := net.SplitHostPort(address) - if err != nil { - return "", 0, err - } - portnum, err := strconv.Atoi(port) - if err != nil { - return "", 0, err - } - if 1 > portnum || portnum > 0xffff { - return "", 0, errors.New("port number out of range " + port) - } - return host, portnum, nil -} diff --git a/vendor/golang.org/x/net/internal/socks/socks.go b/vendor/golang.org/x/net/internal/socks/socks.go deleted file mode 100644 index 84fcc32b6..000000000 --- a/vendor/golang.org/x/net/internal/socks/socks.go +++ /dev/null @@ -1,317 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package socks provides a SOCKS version 5 client implementation. -// -// SOCKS protocol version 5 is defined in RFC 1928. -// Username/Password authentication for SOCKS version 5 is defined in -// RFC 1929. -package socks - -import ( - "context" - "errors" - "io" - "net" - "strconv" -) - -// A Command represents a SOCKS command. -type Command int - -func (cmd Command) String() string { - switch cmd { - case CmdConnect: - return "socks connect" - case cmdBind: - return "socks bind" - default: - return "socks " + strconv.Itoa(int(cmd)) - } -} - -// An AuthMethod represents a SOCKS authentication method. -type AuthMethod int - -// A Reply represents a SOCKS command reply code. -type Reply int - -func (code Reply) String() string { - switch code { - case StatusSucceeded: - return "succeeded" - case 0x01: - return "general SOCKS server failure" - case 0x02: - return "connection not allowed by ruleset" - case 0x03: - return "network unreachable" - case 0x04: - return "host unreachable" - case 0x05: - return "connection refused" - case 0x06: - return "TTL expired" - case 0x07: - return "command not supported" - case 0x08: - return "address type not supported" - default: - return "unknown code: " + strconv.Itoa(int(code)) - } -} - -// Wire protocol constants. -const ( - Version5 = 0x05 - - AddrTypeIPv4 = 0x01 - AddrTypeFQDN = 0x03 - AddrTypeIPv6 = 0x04 - - CmdConnect Command = 0x01 // establishes an active-open forward proxy connection - cmdBind Command = 0x02 // establishes a passive-open forward proxy connection - - AuthMethodNotRequired AuthMethod = 0x00 // no authentication required - AuthMethodUsernamePassword AuthMethod = 0x02 // use username/password - AuthMethodNoAcceptableMethods AuthMethod = 0xff // no acceptable authentication methods - - StatusSucceeded Reply = 0x00 -) - -// An Addr represents a SOCKS-specific address. -// Either Name or IP is used exclusively. -type Addr struct { - Name string // fully-qualified domain name - IP net.IP - Port int -} - -func (a *Addr) Network() string { return "socks" } - -func (a *Addr) String() string { - if a == nil { - return "" - } - port := strconv.Itoa(a.Port) - if a.IP == nil { - return net.JoinHostPort(a.Name, port) - } - return net.JoinHostPort(a.IP.String(), port) -} - -// A Conn represents a forward proxy connection. -type Conn struct { - net.Conn - - boundAddr net.Addr -} - -// BoundAddr returns the address assigned by the proxy server for -// connecting to the command target address from the proxy server. -func (c *Conn) BoundAddr() net.Addr { - if c == nil { - return nil - } - return c.boundAddr -} - -// A Dialer holds SOCKS-specific options. -type Dialer struct { - cmd Command // either CmdConnect or cmdBind - proxyNetwork string // network between a proxy server and a client - proxyAddress string // proxy server address - - // ProxyDial specifies the optional dial function for - // establishing the transport connection. - ProxyDial func(context.Context, string, string) (net.Conn, error) - - // AuthMethods specifies the list of request authentication - // methods. - // If empty, SOCKS client requests only AuthMethodNotRequired. - AuthMethods []AuthMethod - - // Authenticate specifies the optional authentication - // function. It must be non-nil when AuthMethods is not empty. - // It must return an error when the authentication is failed. - Authenticate func(context.Context, io.ReadWriter, AuthMethod) error -} - -// DialContext connects to the provided address on the provided -// network. -// -// The returned error value may be a net.OpError. When the Op field of -// net.OpError contains "socks", the Source field contains a proxy -// server address and the Addr field contains a command target -// address. -// -// See func Dial of the net package of standard library for a -// description of the network and address parameters. -func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { - if err := d.validateTarget(network, address); err != nil { - proxy, dst, _ := d.pathAddrs(address) - return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} - } - if ctx == nil { - proxy, dst, _ := d.pathAddrs(address) - return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: errors.New("nil context")} - } - var err error - var c net.Conn - if d.ProxyDial != nil { - c, err = d.ProxyDial(ctx, d.proxyNetwork, d.proxyAddress) - } else { - var dd net.Dialer - c, err = dd.DialContext(ctx, d.proxyNetwork, d.proxyAddress) - } - if err != nil { - proxy, dst, _ := d.pathAddrs(address) - return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} - } - a, err := d.connect(ctx, c, address) - if err != nil { - c.Close() - proxy, dst, _ := d.pathAddrs(address) - return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} - } - return &Conn{Conn: c, boundAddr: a}, nil -} - -// DialWithConn initiates a connection from SOCKS server to the target -// network and address using the connection c that is already -// connected to the SOCKS server. -// -// It returns the connection's local address assigned by the SOCKS -// server. -func (d *Dialer) DialWithConn(ctx context.Context, c net.Conn, network, address string) (net.Addr, error) { - if err := d.validateTarget(network, address); err != nil { - proxy, dst, _ := d.pathAddrs(address) - return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} - } - if ctx == nil { - proxy, dst, _ := d.pathAddrs(address) - return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: errors.New("nil context")} - } - a, err := d.connect(ctx, c, address) - if err != nil { - proxy, dst, _ := d.pathAddrs(address) - return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} - } - return a, nil -} - -// Dial connects to the provided address on the provided network. -// -// Unlike DialContext, it returns a raw transport connection instead -// of a forward proxy connection. -// -// Deprecated: Use DialContext or DialWithConn instead. -func (d *Dialer) Dial(network, address string) (net.Conn, error) { - if err := d.validateTarget(network, address); err != nil { - proxy, dst, _ := d.pathAddrs(address) - return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} - } - var err error - var c net.Conn - if d.ProxyDial != nil { - c, err = d.ProxyDial(context.Background(), d.proxyNetwork, d.proxyAddress) - } else { - c, err = net.Dial(d.proxyNetwork, d.proxyAddress) - } - if err != nil { - proxy, dst, _ := d.pathAddrs(address) - return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} - } - if _, err := d.DialWithConn(context.Background(), c, network, address); err != nil { - c.Close() - return nil, err - } - return c, nil -} - -func (d *Dialer) validateTarget(network, address string) error { - switch network { - case "tcp", "tcp6", "tcp4": - default: - return errors.New("network not implemented") - } - switch d.cmd { - case CmdConnect, cmdBind: - default: - return errors.New("command not implemented") - } - return nil -} - -func (d *Dialer) pathAddrs(address string) (proxy, dst net.Addr, err error) { - for i, s := range []string{d.proxyAddress, address} { - host, port, err := splitHostPort(s) - if err != nil { - return nil, nil, err - } - a := &Addr{Port: port} - a.IP = net.ParseIP(host) - if a.IP == nil { - a.Name = host - } - if i == 0 { - proxy = a - } else { - dst = a - } - } - return -} - -// NewDialer returns a new Dialer that dials through the provided -// proxy server's network and address. -func NewDialer(network, address string) *Dialer { - return &Dialer{proxyNetwork: network, proxyAddress: address, cmd: CmdConnect} -} - -const ( - authUsernamePasswordVersion = 0x01 - authStatusSucceeded = 0x00 -) - -// UsernamePassword are the credentials for the username/password -// authentication method. -type UsernamePassword struct { - Username string - Password string -} - -// Authenticate authenticates a pair of username and password with the -// proxy server. -func (up *UsernamePassword) Authenticate(ctx context.Context, rw io.ReadWriter, auth AuthMethod) error { - switch auth { - case AuthMethodNotRequired: - return nil - case AuthMethodUsernamePassword: - if len(up.Username) == 0 || len(up.Username) > 255 || len(up.Password) > 255 { - return errors.New("invalid username/password") - } - b := []byte{authUsernamePasswordVersion} - b = append(b, byte(len(up.Username))) - b = append(b, up.Username...) - b = append(b, byte(len(up.Password))) - b = append(b, up.Password...) - // TODO(mikio): handle IO deadlines and cancelation if - // necessary - if _, err := rw.Write(b); err != nil { - return err - } - if _, err := io.ReadFull(rw, b[:2]); err != nil { - return err - } - if b[0] != authUsernamePasswordVersion { - return errors.New("invalid username/password version") - } - if b[1] != authStatusSucceeded { - return errors.New("username/password authentication failed") - } - return nil - } - return errors.New("unsupported authentication method " + strconv.Itoa(int(auth))) -} diff --git a/vendor/golang.org/x/net/proxy/dial.go b/vendor/golang.org/x/net/proxy/dial.go deleted file mode 100644 index 811c2e4e9..000000000 --- a/vendor/golang.org/x/net/proxy/dial.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package proxy - -import ( - "context" - "net" -) - -// A ContextDialer dials using a context. -type ContextDialer interface { - DialContext(ctx context.Context, network, address string) (net.Conn, error) -} - -// Dial works like DialContext on net.Dialer but using a dialer returned by FromEnvironment. -// -// The passed ctx is only used for returning the Conn, not the lifetime of the Conn. -// -// Custom dialers (registered via RegisterDialerType) that do not implement ContextDialer -// can leak a goroutine for as long as it takes the underlying Dialer implementation to timeout. -// -// A Conn returned from a successful Dial after the context has been cancelled will be immediately closed. -func Dial(ctx context.Context, network, address string) (net.Conn, error) { - d := FromEnvironment() - if xd, ok := d.(ContextDialer); ok { - return xd.DialContext(ctx, network, address) - } - return dialContext(ctx, d, network, address) -} - -// WARNING: this can leak a goroutine for as long as the underlying Dialer implementation takes to timeout -// A Conn returned from a successful Dial after the context has been cancelled will be immediately closed. -func dialContext(ctx context.Context, d Dialer, network, address string) (net.Conn, error) { - var ( - conn net.Conn - done = make(chan struct{}, 1) - err error - ) - go func() { - conn, err = d.Dial(network, address) - close(done) - if conn != nil && ctx.Err() != nil { - conn.Close() - } - }() - select { - case <-ctx.Done(): - err = ctx.Err() - case <-done: - } - return conn, err -} diff --git a/vendor/golang.org/x/net/proxy/direct.go b/vendor/golang.org/x/net/proxy/direct.go deleted file mode 100644 index 3d66bdef9..000000000 --- a/vendor/golang.org/x/net/proxy/direct.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package proxy - -import ( - "context" - "net" -) - -type direct struct{} - -// Direct implements Dialer by making network connections directly using net.Dial or net.DialContext. -var Direct = direct{} - -var ( - _ Dialer = Direct - _ ContextDialer = Direct -) - -// Dial directly invokes net.Dial with the supplied parameters. -func (direct) Dial(network, addr string) (net.Conn, error) { - return net.Dial(network, addr) -} - -// DialContext instantiates a net.Dialer and invokes its DialContext receiver with the supplied parameters. -func (direct) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { - var d net.Dialer - return d.DialContext(ctx, network, addr) -} diff --git a/vendor/golang.org/x/net/proxy/per_host.go b/vendor/golang.org/x/net/proxy/per_host.go deleted file mode 100644 index d7d4b8b6e..000000000 --- a/vendor/golang.org/x/net/proxy/per_host.go +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package proxy - -import ( - "context" - "net" - "strings" -) - -// A PerHost directs connections to a default Dialer unless the host name -// requested matches one of a number of exceptions. -type PerHost struct { - def, bypass Dialer - - bypassNetworks []*net.IPNet - bypassIPs []net.IP - bypassZones []string - bypassHosts []string -} - -// NewPerHost returns a PerHost Dialer that directs connections to either -// defaultDialer or bypass, depending on whether the connection matches one of -// the configured rules. -func NewPerHost(defaultDialer, bypass Dialer) *PerHost { - return &PerHost{ - def: defaultDialer, - bypass: bypass, - } -} - -// Dial connects to the address addr on the given network through either -// defaultDialer or bypass. -func (p *PerHost) Dial(network, addr string) (c net.Conn, err error) { - host, _, err := net.SplitHostPort(addr) - if err != nil { - return nil, err - } - - return p.dialerForRequest(host).Dial(network, addr) -} - -// DialContext connects to the address addr on the given network through either -// defaultDialer or bypass. -func (p *PerHost) DialContext(ctx context.Context, network, addr string) (c net.Conn, err error) { - host, _, err := net.SplitHostPort(addr) - if err != nil { - return nil, err - } - d := p.dialerForRequest(host) - if x, ok := d.(ContextDialer); ok { - return x.DialContext(ctx, network, addr) - } - return dialContext(ctx, d, network, addr) -} - -func (p *PerHost) dialerForRequest(host string) Dialer { - if ip := net.ParseIP(host); ip != nil { - for _, net := range p.bypassNetworks { - if net.Contains(ip) { - return p.bypass - } - } - for _, bypassIP := range p.bypassIPs { - if bypassIP.Equal(ip) { - return p.bypass - } - } - return p.def - } - - for _, zone := range p.bypassZones { - if strings.HasSuffix(host, zone) { - return p.bypass - } - if host == zone[1:] { - // For a zone ".example.com", we match "example.com" - // too. - return p.bypass - } - } - for _, bypassHost := range p.bypassHosts { - if bypassHost == host { - return p.bypass - } - } - return p.def -} - -// AddFromString parses a string that contains comma-separated values -// specifying hosts that should use the bypass proxy. Each value is either an -// IP address, a CIDR range, a zone (*.example.com) or a host name -// (localhost). A best effort is made to parse the string and errors are -// ignored. -func (p *PerHost) AddFromString(s string) { - hosts := strings.Split(s, ",") - for _, host := range hosts { - host = strings.TrimSpace(host) - if len(host) == 0 { - continue - } - if strings.Contains(host, "/") { - // We assume that it's a CIDR address like 127.0.0.0/8 - if _, net, err := net.ParseCIDR(host); err == nil { - p.AddNetwork(net) - } - continue - } - if ip := net.ParseIP(host); ip != nil { - p.AddIP(ip) - continue - } - if strings.HasPrefix(host, "*.") { - p.AddZone(host[1:]) - continue - } - p.AddHost(host) - } -} - -// AddIP specifies an IP address that will use the bypass proxy. Note that -// this will only take effect if a literal IP address is dialed. A connection -// to a named host will never match an IP. -func (p *PerHost) AddIP(ip net.IP) { - p.bypassIPs = append(p.bypassIPs, ip) -} - -// AddNetwork specifies an IP range that will use the bypass proxy. Note that -// this will only take effect if a literal IP address is dialed. A connection -// to a named host will never match. -func (p *PerHost) AddNetwork(net *net.IPNet) { - p.bypassNetworks = append(p.bypassNetworks, net) -} - -// AddZone specifies a DNS suffix that will use the bypass proxy. A zone of -// "example.com" matches "example.com" and all of its subdomains. -func (p *PerHost) AddZone(zone string) { - zone = strings.TrimSuffix(zone, ".") - if !strings.HasPrefix(zone, ".") { - zone = "." + zone - } - p.bypassZones = append(p.bypassZones, zone) -} - -// AddHost specifies a host name that will use the bypass proxy. -func (p *PerHost) AddHost(host string) { - host = strings.TrimSuffix(host, ".") - p.bypassHosts = append(p.bypassHosts, host) -} diff --git a/vendor/golang.org/x/net/proxy/proxy.go b/vendor/golang.org/x/net/proxy/proxy.go deleted file mode 100644 index 9ff4b9a77..000000000 --- a/vendor/golang.org/x/net/proxy/proxy.go +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package proxy provides support for a variety of protocols to proxy network -// data. -package proxy // import "golang.org/x/net/proxy" - -import ( - "errors" - "net" - "net/url" - "os" - "sync" -) - -// A Dialer is a means to establish a connection. -// Custom dialers should also implement ContextDialer. -type Dialer interface { - // Dial connects to the given address via the proxy. - Dial(network, addr string) (c net.Conn, err error) -} - -// Auth contains authentication parameters that specific Dialers may require. -type Auth struct { - User, Password string -} - -// FromEnvironment returns the dialer specified by the proxy-related -// variables in the environment and makes underlying connections -// directly. -func FromEnvironment() Dialer { - return FromEnvironmentUsing(Direct) -} - -// FromEnvironmentUsing returns the dialer specify by the proxy-related -// variables in the environment and makes underlying connections -// using the provided forwarding Dialer (for instance, a *net.Dialer -// with desired configuration). -func FromEnvironmentUsing(forward Dialer) Dialer { - allProxy := allProxyEnv.Get() - if len(allProxy) == 0 { - return forward - } - - proxyURL, err := url.Parse(allProxy) - if err != nil { - return forward - } - proxy, err := FromURL(proxyURL, forward) - if err != nil { - return forward - } - - noProxy := noProxyEnv.Get() - if len(noProxy) == 0 { - return proxy - } - - perHost := NewPerHost(proxy, forward) - perHost.AddFromString(noProxy) - return perHost -} - -// proxySchemes is a map from URL schemes to a function that creates a Dialer -// from a URL with such a scheme. -var proxySchemes map[string]func(*url.URL, Dialer) (Dialer, error) - -// RegisterDialerType takes a URL scheme and a function to generate Dialers from -// a URL with that scheme and a forwarding Dialer. Registered schemes are used -// by FromURL. -func RegisterDialerType(scheme string, f func(*url.URL, Dialer) (Dialer, error)) { - if proxySchemes == nil { - proxySchemes = make(map[string]func(*url.URL, Dialer) (Dialer, error)) - } - proxySchemes[scheme] = f -} - -// FromURL returns a Dialer given a URL specification and an underlying -// Dialer for it to make network requests. -func FromURL(u *url.URL, forward Dialer) (Dialer, error) { - var auth *Auth - if u.User != nil { - auth = new(Auth) - auth.User = u.User.Username() - if p, ok := u.User.Password(); ok { - auth.Password = p - } - } - - switch u.Scheme { - case "socks5", "socks5h": - addr := u.Hostname() - port := u.Port() - if port == "" { - port = "1080" - } - return SOCKS5("tcp", net.JoinHostPort(addr, port), auth, forward) - } - - // If the scheme doesn't match any of the built-in schemes, see if it - // was registered by another package. - if proxySchemes != nil { - if f, ok := proxySchemes[u.Scheme]; ok { - return f(u, forward) - } - } - - return nil, errors.New("proxy: unknown scheme: " + u.Scheme) -} - -var ( - allProxyEnv = &envOnce{ - names: []string{"ALL_PROXY", "all_proxy"}, - } - noProxyEnv = &envOnce{ - names: []string{"NO_PROXY", "no_proxy"}, - } -) - -// envOnce looks up an environment variable (optionally by multiple -// names) once. It mitigates expensive lookups on some platforms -// (e.g. Windows). -// (Borrowed from net/http/transport.go) -type envOnce struct { - names []string - once sync.Once - val string -} - -func (e *envOnce) Get() string { - e.once.Do(e.init) - return e.val -} - -func (e *envOnce) init() { - for _, n := range e.names { - e.val = os.Getenv(n) - if e.val != "" { - return - } - } -} - -// reset is used by tests -func (e *envOnce) reset() { - e.once = sync.Once{} - e.val = "" -} diff --git a/vendor/golang.org/x/net/proxy/socks5.go b/vendor/golang.org/x/net/proxy/socks5.go deleted file mode 100644 index c91651f96..000000000 --- a/vendor/golang.org/x/net/proxy/socks5.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package proxy - -import ( - "context" - "net" - - "golang.org/x/net/internal/socks" -) - -// SOCKS5 returns a Dialer that makes SOCKSv5 connections to the given -// address with an optional username and password. -// See RFC 1928 and RFC 1929. -func SOCKS5(network, address string, auth *Auth, forward Dialer) (Dialer, error) { - d := socks.NewDialer(network, address) - if forward != nil { - if f, ok := forward.(ContextDialer); ok { - d.ProxyDial = func(ctx context.Context, network string, address string) (net.Conn, error) { - return f.DialContext(ctx, network, address) - } - } else { - d.ProxyDial = func(ctx context.Context, network string, address string) (net.Conn, error) { - return dialContext(ctx, forward, network, address) - } - } - } - if auth != nil { - up := socks.UsernamePassword{ - Username: auth.User, - Password: auth.Password, - } - d.AuthMethods = []socks.AuthMethod{ - socks.AuthMethodNotRequired, - socks.AuthMethodUsernamePassword, - } - d.Authenticate = up.Authenticate - } - return d, nil -} diff --git a/vendor/modules.txt b/vendor/modules.txt index 68fb93086..2f8c861a2 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -364,8 +364,8 @@ github.com/gorilla/securecookie # github.com/gorilla/sessions v1.2.2 ## explicit; go 1.20 github.com/gorilla/sessions -# github.com/gorilla/websocket v1.5.2 -## explicit; go 1.20 +# github.com/gorilla/websocket v1.5.3 +## explicit; go 1.12 github.com/gorilla/websocket # github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 ## explicit; go 1.21 @@ -1109,11 +1109,9 @@ golang.org/x/net/http2/hpack golang.org/x/net/idna golang.org/x/net/internal/iana golang.org/x/net/internal/socket -golang.org/x/net/internal/socks golang.org/x/net/internal/timeseries golang.org/x/net/ipv4 golang.org/x/net/ipv6 -golang.org/x/net/proxy golang.org/x/net/publicsuffix golang.org/x/net/trace # golang.org/x/oauth2 v0.24.0 From 934e895ec06acc41a8cf22e3462e7ed1edcf994e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 11:14:48 +0000 Subject: [PATCH 03/26] [chore]: Bump github.com/stretchr/testify from 1.9.0 to 1.10.0 (#3564) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.9.0 to 1.10.0. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.9.0...v1.10.0) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 3 +- .../testify/assert/assertion_compare.go | 35 +- .../testify/assert/assertion_format.go | 34 +- .../testify/assert/assertion_forward.go | 68 ++- .../testify/assert/assertion_order.go | 10 +- .../stretchr/testify/assert/assertions.go | 157 +++++-- .../testify/assert/yaml/yaml_custom.go | 25 + .../testify/assert/yaml/yaml_default.go | 37 ++ .../stretchr/testify/assert/yaml/yaml_fail.go | 18 + .../stretchr/testify/require/require.go | 432 ++++++++++-------- .../stretchr/testify/require/require.go.tmpl | 2 +- .../testify/require/require_forward.go | 68 ++- .../stretchr/testify/require/requirements.go | 2 +- .../github.com/stretchr/testify/suite/doc.go | 4 + vendor/modules.txt | 3 +- 16 files changed, 634 insertions(+), 266 deletions(-) create mode 100644 vendor/github.com/stretchr/testify/assert/yaml/yaml_custom.go create mode 100644 vendor/github.com/stretchr/testify/assert/yaml/yaml_default.go create mode 100644 vendor/github.com/stretchr/testify/assert/yaml/yaml_fail.go diff --git a/go.mod b/go.mod index 261aefba4..bc1d56a46 100644 --- a/go.mod +++ b/go.mod @@ -64,7 +64,7 @@ require ( github.com/prometheus/client_golang v1.20.5 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/superseriousbusiness/activity v1.9.0-gts github.com/superseriousbusiness/httpsig v1.2.0-SSB github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8 diff --git a/go.sum b/go.sum index a59ca6946..78d023286 100644 --- a/go.sum +++ b/go.sum @@ -527,8 +527,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/superseriousbusiness/activity v1.9.0-gts h1:qWMDeiGdnVi+XG7CfuM7ET87qe9adousU6utWItBX/o= diff --git a/vendor/github.com/stretchr/testify/assert/assertion_compare.go b/vendor/github.com/stretchr/testify/assert/assertion_compare.go index 4d4b4aad6..7e19eba09 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_compare.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_compare.go @@ -7,10 +7,13 @@ import ( "time" ) -type CompareType int +// Deprecated: CompareType has only ever been for internal use and has accidentally been published since v1.6.0. Do not use it. +type CompareType = compareResult + +type compareResult int const ( - compareLess CompareType = iota - 1 + compareLess compareResult = iota - 1 compareEqual compareGreater ) @@ -39,7 +42,7 @@ var ( bytesType = reflect.TypeOf([]byte{}) ) -func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { +func compare(obj1, obj2 interface{}, kind reflect.Kind) (compareResult, bool) { obj1Value := reflect.ValueOf(obj1) obj2Value := reflect.ValueOf(obj2) @@ -325,7 +328,13 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { timeObj2 = obj2Value.Convert(timeType).Interface().(time.Time) } - return compare(timeObj1.UnixNano(), timeObj2.UnixNano(), reflect.Int64) + if timeObj1.Before(timeObj2) { + return compareLess, true + } + if timeObj1.Equal(timeObj2) { + return compareEqual, true + } + return compareGreater, true } case reflect.Slice: { @@ -345,7 +354,7 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { bytesObj2 = obj2Value.Convert(bytesType).Interface().([]byte) } - return CompareType(bytes.Compare(bytesObj1, bytesObj2)), true + return compareResult(bytes.Compare(bytesObj1, bytesObj2)), true } case reflect.Uintptr: { @@ -381,7 +390,7 @@ func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface if h, ok := t.(tHelper); ok { h.Helper() } - return compareTwoValues(t, e1, e2, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs...) + return compareTwoValues(t, e1, e2, []compareResult{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs...) } // GreaterOrEqual asserts that the first element is greater than or equal to the second @@ -394,7 +403,7 @@ func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...in if h, ok := t.(tHelper); ok { h.Helper() } - return compareTwoValues(t, e1, e2, []CompareType{compareGreater, compareEqual}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs...) + return compareTwoValues(t, e1, e2, []compareResult{compareGreater, compareEqual}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs...) } // Less asserts that the first element is less than the second @@ -406,7 +415,7 @@ func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) if h, ok := t.(tHelper); ok { h.Helper() } - return compareTwoValues(t, e1, e2, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs...) + return compareTwoValues(t, e1, e2, []compareResult{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs...) } // LessOrEqual asserts that the first element is less than or equal to the second @@ -419,7 +428,7 @@ func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...inter if h, ok := t.(tHelper); ok { h.Helper() } - return compareTwoValues(t, e1, e2, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...) + return compareTwoValues(t, e1, e2, []compareResult{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...) } // Positive asserts that the specified element is positive @@ -431,7 +440,7 @@ func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { h.Helper() } zero := reflect.Zero(reflect.TypeOf(e)) - return compareTwoValues(t, e, zero.Interface(), []CompareType{compareGreater}, "\"%v\" is not positive", msgAndArgs...) + return compareTwoValues(t, e, zero.Interface(), []compareResult{compareGreater}, "\"%v\" is not positive", msgAndArgs...) } // Negative asserts that the specified element is negative @@ -443,10 +452,10 @@ func Negative(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { h.Helper() } zero := reflect.Zero(reflect.TypeOf(e)) - return compareTwoValues(t, e, zero.Interface(), []CompareType{compareLess}, "\"%v\" is not negative", msgAndArgs...) + return compareTwoValues(t, e, zero.Interface(), []compareResult{compareLess}, "\"%v\" is not negative", msgAndArgs...) } -func compareTwoValues(t TestingT, e1 interface{}, e2 interface{}, allowedComparesResults []CompareType, failMessage string, msgAndArgs ...interface{}) bool { +func compareTwoValues(t TestingT, e1 interface{}, e2 interface{}, allowedComparesResults []compareResult, failMessage string, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() } @@ -469,7 +478,7 @@ func compareTwoValues(t TestingT, e1 interface{}, e2 interface{}, allowedCompare return true } -func containsValue(values []CompareType, value CompareType) bool { +func containsValue(values []compareResult, value compareResult) bool { for _, v := range values { if v == value { return true diff --git a/vendor/github.com/stretchr/testify/assert/assertion_format.go b/vendor/github.com/stretchr/testify/assert/assertion_format.go index 3ddab109a..190634165 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_format.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_format.go @@ -104,8 +104,8 @@ func EqualExportedValuesf(t TestingT, expected interface{}, actual interface{}, return EqualExportedValues(t, expected, actual, append([]interface{}{msg}, args...)...) } -// EqualValuesf asserts that two objects are equal or convertible to the same types -// and equal. +// EqualValuesf asserts that two objects are equal or convertible to the larger +// type and equal. // // assert.EqualValuesf(t, uint32(123), int32(123), "error message %s", "formatted") func EqualValuesf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool { @@ -186,7 +186,7 @@ func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick // assert.EventuallyWithTf(t, func(c *assert.CollectT, "error message %s", "formatted") { // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") -// }, 1*time.Second, 10*time.Second, "external state has not changed to 'true'; still false") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") func EventuallyWithTf(t TestingT, condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() @@ -568,6 +568,23 @@ func NotContainsf(t TestingT, s interface{}, contains interface{}, msg string, a return NotContains(t, s, contains, append([]interface{}{msg}, args...)...) } +// NotElementsMatchf asserts that the specified listA(array, slice...) is NOT equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should not match. +// This is an inverse of ElementsMatch. +// +// assert.NotElementsMatchf(t, [1, 1, 2, 3], [1, 1, 2, 3], "error message %s", "formatted") -> false +// +// assert.NotElementsMatchf(t, [1, 1, 2, 3], [1, 2, 3], "error message %s", "formatted") -> true +// +// assert.NotElementsMatchf(t, [1, 2, 3], [1, 2, 4], "error message %s", "formatted") -> true +func NotElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotElementsMatch(t, listA, listB, append([]interface{}{msg}, args...)...) +} + // NotEmptyf asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either // a slice or a channel with len == 0. // @@ -604,7 +621,16 @@ func NotEqualValuesf(t TestingT, expected interface{}, actual interface{}, msg s return NotEqualValues(t, expected, actual, append([]interface{}{msg}, args...)...) } -// NotErrorIsf asserts that at none of the errors in err's chain matches target. +// NotErrorAsf asserts that none of the errors in err's chain matches target, +// but if so, sets target to that error value. +func NotErrorAsf(t TestingT, err error, target interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotErrorAs(t, err, target, append([]interface{}{msg}, args...)...) +} + +// NotErrorIsf asserts that none of the errors in err's chain matches target. // This is a wrapper for errors.Is. func NotErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { diff --git a/vendor/github.com/stretchr/testify/assert/assertion_forward.go b/vendor/github.com/stretchr/testify/assert/assertion_forward.go index a84e09bd4..21629087b 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_forward.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_forward.go @@ -186,8 +186,8 @@ func (a *Assertions) EqualExportedValuesf(expected interface{}, actual interface return EqualExportedValuesf(a.t, expected, actual, msg, args...) } -// EqualValues asserts that two objects are equal or convertible to the same types -// and equal. +// EqualValues asserts that two objects are equal or convertible to the larger +// type and equal. // // a.EqualValues(uint32(123), int32(123)) func (a *Assertions) EqualValues(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { @@ -197,8 +197,8 @@ func (a *Assertions) EqualValues(expected interface{}, actual interface{}, msgAn return EqualValues(a.t, expected, actual, msgAndArgs...) } -// EqualValuesf asserts that two objects are equal or convertible to the same types -// and equal. +// EqualValuesf asserts that two objects are equal or convertible to the larger +// type and equal. // // a.EqualValuesf(uint32(123), int32(123), "error message %s", "formatted") func (a *Assertions) EqualValuesf(expected interface{}, actual interface{}, msg string, args ...interface{}) bool { @@ -336,7 +336,7 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti // a.EventuallyWithT(func(c *assert.CollectT) { // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") -// }, 1*time.Second, 10*time.Second, "external state has not changed to 'true'; still false") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") func (a *Assertions) EventuallyWithT(condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -361,7 +361,7 @@ func (a *Assertions) EventuallyWithT(condition func(collect *CollectT), waitFor // a.EventuallyWithTf(func(c *assert.CollectT, "error message %s", "formatted") { // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") -// }, 1*time.Second, 10*time.Second, "external state has not changed to 'true'; still false") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") func (a *Assertions) EventuallyWithTf(condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1128,6 +1128,40 @@ func (a *Assertions) NotContainsf(s interface{}, contains interface{}, msg strin return NotContainsf(a.t, s, contains, msg, args...) } +// NotElementsMatch asserts that the specified listA(array, slice...) is NOT equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should not match. +// This is an inverse of ElementsMatch. +// +// a.NotElementsMatch([1, 1, 2, 3], [1, 1, 2, 3]) -> false +// +// a.NotElementsMatch([1, 1, 2, 3], [1, 2, 3]) -> true +// +// a.NotElementsMatch([1, 2, 3], [1, 2, 4]) -> true +func (a *Assertions) NotElementsMatch(listA interface{}, listB interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotElementsMatch(a.t, listA, listB, msgAndArgs...) +} + +// NotElementsMatchf asserts that the specified listA(array, slice...) is NOT equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should not match. +// This is an inverse of ElementsMatch. +// +// a.NotElementsMatchf([1, 1, 2, 3], [1, 1, 2, 3], "error message %s", "formatted") -> false +// +// a.NotElementsMatchf([1, 1, 2, 3], [1, 2, 3], "error message %s", "formatted") -> true +// +// a.NotElementsMatchf([1, 2, 3], [1, 2, 4], "error message %s", "formatted") -> true +func (a *Assertions) NotElementsMatchf(listA interface{}, listB interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotElementsMatchf(a.t, listA, listB, msg, args...) +} + // NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either // a slice or a channel with len == 0. // @@ -1200,7 +1234,25 @@ func (a *Assertions) NotEqualf(expected interface{}, actual interface{}, msg str return NotEqualf(a.t, expected, actual, msg, args...) } -// NotErrorIs asserts that at none of the errors in err's chain matches target. +// NotErrorAs asserts that none of the errors in err's chain matches target, +// but if so, sets target to that error value. +func (a *Assertions) NotErrorAs(err error, target interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotErrorAs(a.t, err, target, msgAndArgs...) +} + +// NotErrorAsf asserts that none of the errors in err's chain matches target, +// but if so, sets target to that error value. +func (a *Assertions) NotErrorAsf(err error, target interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotErrorAsf(a.t, err, target, msg, args...) +} + +// NotErrorIs asserts that none of the errors in err's chain matches target. // This is a wrapper for errors.Is. func (a *Assertions) NotErrorIs(err error, target error, msgAndArgs ...interface{}) bool { if h, ok := a.t.(tHelper); ok { @@ -1209,7 +1261,7 @@ func (a *Assertions) NotErrorIs(err error, target error, msgAndArgs ...interface return NotErrorIs(a.t, err, target, msgAndArgs...) } -// NotErrorIsf asserts that at none of the errors in err's chain matches target. +// NotErrorIsf asserts that none of the errors in err's chain matches target. // This is a wrapper for errors.Is. func (a *Assertions) NotErrorIsf(err error, target error, msg string, args ...interface{}) bool { if h, ok := a.t.(tHelper); ok { diff --git a/vendor/github.com/stretchr/testify/assert/assertion_order.go b/vendor/github.com/stretchr/testify/assert/assertion_order.go index 00df62a05..1d2f71824 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_order.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_order.go @@ -6,7 +6,7 @@ import ( ) // isOrdered checks that collection contains orderable elements. -func isOrdered(t TestingT, object interface{}, allowedComparesResults []CompareType, failMessage string, msgAndArgs ...interface{}) bool { +func isOrdered(t TestingT, object interface{}, allowedComparesResults []compareResult, failMessage string, msgAndArgs ...interface{}) bool { objKind := reflect.TypeOf(object).Kind() if objKind != reflect.Slice && objKind != reflect.Array { return false @@ -50,7 +50,7 @@ func isOrdered(t TestingT, object interface{}, allowedComparesResults []CompareT // assert.IsIncreasing(t, []float{1, 2}) // assert.IsIncreasing(t, []string{"a", "b"}) func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs...) + return isOrdered(t, object, []compareResult{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs...) } // IsNonIncreasing asserts that the collection is not increasing @@ -59,7 +59,7 @@ func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) boo // assert.IsNonIncreasing(t, []float{2, 1}) // assert.IsNonIncreasing(t, []string{"b", "a"}) func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareEqual, compareGreater}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs...) + return isOrdered(t, object, []compareResult{compareEqual, compareGreater}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs...) } // IsDecreasing asserts that the collection is decreasing @@ -68,7 +68,7 @@ func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) // assert.IsDecreasing(t, []float{2, 1}) // assert.IsDecreasing(t, []string{"b", "a"}) func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs...) + return isOrdered(t, object, []compareResult{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs...) } // IsNonDecreasing asserts that the collection is not decreasing @@ -77,5 +77,5 @@ func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) boo // assert.IsNonDecreasing(t, []float{1, 2}) // assert.IsNonDecreasing(t, []string{"a", "b"}) func IsNonDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...) + return isOrdered(t, object, []compareResult{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...) } diff --git a/vendor/github.com/stretchr/testify/assert/assertions.go b/vendor/github.com/stretchr/testify/assert/assertions.go index 0b7570f21..4e91332bb 100644 --- a/vendor/github.com/stretchr/testify/assert/assertions.go +++ b/vendor/github.com/stretchr/testify/assert/assertions.go @@ -19,7 +19,9 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/pmezard/go-difflib/difflib" - "gopkg.in/yaml.v3" + + // Wrapper around gopkg.in/yaml.v3 + "github.com/stretchr/testify/assert/yaml" ) //go:generate sh -c "cd ../_codegen && go build && cd - && ../_codegen/_codegen -output-package=assert -template=assertion_format.go.tmpl" @@ -45,6 +47,10 @@ type BoolAssertionFunc func(TestingT, bool, ...interface{}) bool // for table driven tests. type ErrorAssertionFunc func(TestingT, error, ...interface{}) bool +// PanicAssertionFunc is a common function prototype when validating a panic value. Can be useful +// for table driven tests. +type PanicAssertionFunc = func(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool + // Comparison is a custom function that returns true on success and false on failure type Comparison func() (success bool) @@ -496,7 +502,13 @@ func Same(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) b h.Helper() } - if !samePointers(expected, actual) { + same, ok := samePointers(expected, actual) + if !ok { + return Fail(t, "Both arguments must be pointers", msgAndArgs...) + } + + if !same { + // both are pointers but not the same type & pointing to the same address return Fail(t, fmt.Sprintf("Not same: \n"+ "expected: %p %#v\n"+ "actual : %p %#v", expected, expected, actual, actual), msgAndArgs...) @@ -516,7 +528,13 @@ func NotSame(t TestingT, expected, actual interface{}, msgAndArgs ...interface{} h.Helper() } - if samePointers(expected, actual) { + same, ok := samePointers(expected, actual) + if !ok { + //fails when the arguments are not pointers + return !(Fail(t, "Both arguments must be pointers", msgAndArgs...)) + } + + if same { return Fail(t, fmt.Sprintf( "Expected and actual point to the same object: %p %#v", expected, expected), msgAndArgs...) @@ -524,21 +542,23 @@ func NotSame(t TestingT, expected, actual interface{}, msgAndArgs ...interface{} return true } -// samePointers compares two generic interface objects and returns whether -// they point to the same object -func samePointers(first, second interface{}) bool { +// samePointers checks if two generic interface objects are pointers of the same +// type pointing to the same object. It returns two values: same indicating if +// they are the same type and point to the same object, and ok indicating that +// both inputs are pointers. +func samePointers(first, second interface{}) (same bool, ok bool) { firstPtr, secondPtr := reflect.ValueOf(first), reflect.ValueOf(second) if firstPtr.Kind() != reflect.Ptr || secondPtr.Kind() != reflect.Ptr { - return false + return false, false //not both are pointers } firstType, secondType := reflect.TypeOf(first), reflect.TypeOf(second) if firstType != secondType { - return false + return false, true // both are pointers, but of different types } // compare pointer addresses - return first == second + return first == second, true } // formatUnequalValues takes two values of arbitrary types and returns string @@ -572,8 +592,8 @@ func truncatingFormat(data interface{}) string { return value } -// EqualValues asserts that two objects are equal or convertible to the same types -// and equal. +// EqualValues asserts that two objects are equal or convertible to the larger +// type and equal. // // assert.EqualValues(t, uint32(123), int32(123)) func EqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { @@ -615,21 +635,6 @@ func EqualExportedValues(t TestingT, expected, actual interface{}, msgAndArgs .. return Fail(t, fmt.Sprintf("Types expected to match exactly\n\t%v != %v", aType, bType), msgAndArgs...) } - if aType.Kind() == reflect.Ptr { - aType = aType.Elem() - } - if bType.Kind() == reflect.Ptr { - bType = bType.Elem() - } - - if aType.Kind() != reflect.Struct { - return Fail(t, fmt.Sprintf("Types expected to both be struct or pointer to struct \n\t%v != %v", aType.Kind(), reflect.Struct), msgAndArgs...) - } - - if bType.Kind() != reflect.Struct { - return Fail(t, fmt.Sprintf("Types expected to both be struct or pointer to struct \n\t%v != %v", bType.Kind(), reflect.Struct), msgAndArgs...) - } - expected = copyExportedFields(expected) actual = copyExportedFields(actual) @@ -1170,6 +1175,39 @@ func formatListDiff(listA, listB interface{}, extraA, extraB []interface{}) stri return msg.String() } +// NotElementsMatch asserts that the specified listA(array, slice...) is NOT equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should not match. +// This is an inverse of ElementsMatch. +// +// assert.NotElementsMatch(t, [1, 1, 2, 3], [1, 1, 2, 3]) -> false +// +// assert.NotElementsMatch(t, [1, 1, 2, 3], [1, 2, 3]) -> true +// +// assert.NotElementsMatch(t, [1, 2, 3], [1, 2, 4]) -> true +func NotElementsMatch(t TestingT, listA, listB interface{}, msgAndArgs ...interface{}) (ok bool) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if isEmpty(listA) && isEmpty(listB) { + return Fail(t, "listA and listB contain the same elements", msgAndArgs) + } + + if !isList(t, listA, msgAndArgs...) { + return Fail(t, "listA is not a list type", msgAndArgs...) + } + if !isList(t, listB, msgAndArgs...) { + return Fail(t, "listB is not a list type", msgAndArgs...) + } + + extraA, extraB := diffLists(listA, listB) + if len(extraA) == 0 && len(extraB) == 0 { + return Fail(t, "listA and listB contain the same elements", msgAndArgs) + } + + return true +} + // Condition uses a Comparison to assert a complex condition. func Condition(t TestingT, comp Comparison, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { @@ -1488,6 +1526,9 @@ func InEpsilon(t TestingT, expected, actual interface{}, epsilon float64, msgAnd if err != nil { return Fail(t, err.Error(), msgAndArgs...) } + if math.IsNaN(actualEpsilon) { + return Fail(t, "relative error is NaN", msgAndArgs...) + } if actualEpsilon > epsilon { return Fail(t, fmt.Sprintf("Relative error is too high: %#v (expected)\n"+ " < %#v (actual)", epsilon, actualEpsilon), msgAndArgs...) @@ -1611,7 +1652,6 @@ func ErrorContains(t TestingT, theError error, contains string, msgAndArgs ...in // matchRegexp return true if a specified regexp matches a string. func matchRegexp(rx interface{}, str interface{}) bool { - var r *regexp.Regexp if rr, ok := rx.(*regexp.Regexp); ok { r = rr @@ -1619,7 +1659,14 @@ func matchRegexp(rx interface{}, str interface{}) bool { r = regexp.MustCompile(fmt.Sprint(rx)) } - return (r.FindStringIndex(fmt.Sprint(str)) != nil) + switch v := str.(type) { + case []byte: + return r.Match(v) + case string: + return r.MatchString(v) + default: + return r.MatchString(fmt.Sprint(v)) + } } @@ -1872,7 +1919,7 @@ var spewConfigStringerEnabled = spew.ConfigState{ MaxDepth: 10, } -type tHelper interface { +type tHelper = interface { Helper() } @@ -1911,6 +1958,9 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t // CollectT implements the TestingT interface and collects all errors. type CollectT struct { + // A slice of errors. Non-nil slice denotes a failure. + // If it's non-nil but len(c.errors) == 0, this is also a failure + // obtained by direct c.FailNow() call. errors []error } @@ -1919,9 +1969,10 @@ func (c *CollectT) Errorf(format string, args ...interface{}) { c.errors = append(c.errors, fmt.Errorf(format, args...)) } -// FailNow panics. -func (*CollectT) FailNow() { - panic("Assertion failed") +// FailNow stops execution by calling runtime.Goexit. +func (c *CollectT) FailNow() { + c.fail() + runtime.Goexit() } // Deprecated: That was a method for internal usage that should not have been published. Now just panics. @@ -1934,6 +1985,16 @@ func (*CollectT) Copy(TestingT) { panic("Copy() is deprecated") } +func (c *CollectT) fail() { + if !c.failed() { + c.errors = []error{} // Make it non-nil to mark a failure. + } +} + +func (c *CollectT) failed() bool { + return c.errors != nil +} + // EventuallyWithT asserts that given condition will be met in waitFor time, // periodically checking target function each tick. In contrast to Eventually, // it supplies a CollectT to the condition function, so that the condition @@ -1951,14 +2012,14 @@ func (*CollectT) Copy(TestingT) { // assert.EventuallyWithT(t, func(c *assert.CollectT) { // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") -// }, 1*time.Second, 10*time.Second, "external state has not changed to 'true'; still false") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() } var lastFinishedTickErrs []error - ch := make(chan []error, 1) + ch := make(chan *CollectT, 1) timer := time.NewTimer(waitFor) defer timer.Stop() @@ -1978,16 +2039,16 @@ func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time go func() { collect := new(CollectT) defer func() { - ch <- collect.errors + ch <- collect }() condition(collect) }() - case errs := <-ch: - if len(errs) == 0 { + case collect := <-ch: + if !collect.failed() { return true } // Keep the errors from the last ended condition, so that they can be copied to t if timeout is reached. - lastFinishedTickErrs = errs + lastFinishedTickErrs = collect.errors tick = ticker.C } } @@ -2049,7 +2110,7 @@ func ErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool { ), msgAndArgs...) } -// NotErrorIs asserts that at none of the errors in err's chain matches target. +// NotErrorIs asserts that none of the errors in err's chain matches target. // This is a wrapper for errors.Is. func NotErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { @@ -2090,6 +2151,24 @@ func ErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{ ), msgAndArgs...) } +// NotErrorAs asserts that none of the errors in err's chain matches target, +// but if so, sets target to that error value. +func NotErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if !errors.As(err, target) { + return true + } + + chain := buildErrorChainString(err) + + return Fail(t, fmt.Sprintf("Target error should not be in err chain:\n"+ + "found: %q\n"+ + "in chain: %s", target, chain, + ), msgAndArgs...) +} + func buildErrorChainString(err error) string { if err == nil { return "" diff --git a/vendor/github.com/stretchr/testify/assert/yaml/yaml_custom.go b/vendor/github.com/stretchr/testify/assert/yaml/yaml_custom.go new file mode 100644 index 000000000..baa0cc7d7 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/yaml/yaml_custom.go @@ -0,0 +1,25 @@ +//go:build testify_yaml_custom && !testify_yaml_fail && !testify_yaml_default +// +build testify_yaml_custom,!testify_yaml_fail,!testify_yaml_default + +// Package yaml is an implementation of YAML functions that calls a pluggable implementation. +// +// This implementation is selected with the testify_yaml_custom build tag. +// +// go test -tags testify_yaml_custom +// +// This implementation can be used at build time to replace the default implementation +// to avoid linking with [gopkg.in/yaml.v3]. +// +// In your test package: +// +// import assertYaml "github.com/stretchr/testify/assert/yaml" +// +// func init() { +// assertYaml.Unmarshal = func (in []byte, out interface{}) error { +// // ... +// return nil +// } +// } +package yaml + +var Unmarshal func(in []byte, out interface{}) error diff --git a/vendor/github.com/stretchr/testify/assert/yaml/yaml_default.go b/vendor/github.com/stretchr/testify/assert/yaml/yaml_default.go new file mode 100644 index 000000000..b83c6cf64 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/yaml/yaml_default.go @@ -0,0 +1,37 @@ +//go:build !testify_yaml_fail && !testify_yaml_custom +// +build !testify_yaml_fail,!testify_yaml_custom + +// Package yaml is just an indirection to handle YAML deserialization. +// +// This package is just an indirection that allows the builder to override the +// indirection with an alternative implementation of this package that uses +// another implementation of YAML deserialization. This allows to not either not +// use YAML deserialization at all, or to use another implementation than +// [gopkg.in/yaml.v3] (for example for license compatibility reasons, see [PR #1120]). +// +// Alternative implementations are selected using build tags: +// +// - testify_yaml_fail: [Unmarshal] always fails with an error +// - testify_yaml_custom: [Unmarshal] is a variable. Caller must initialize it +// before calling any of [github.com/stretchr/testify/assert.YAMLEq] or +// [github.com/stretchr/testify/assert.YAMLEqf]. +// +// Usage: +// +// go test -tags testify_yaml_fail +// +// You can check with "go list" which implementation is linked: +// +// go list -f '{{.Imports}}' github.com/stretchr/testify/assert/yaml +// go list -tags testify_yaml_fail -f '{{.Imports}}' github.com/stretchr/testify/assert/yaml +// go list -tags testify_yaml_custom -f '{{.Imports}}' github.com/stretchr/testify/assert/yaml +// +// [PR #1120]: https://github.com/stretchr/testify/pull/1120 +package yaml + +import goyaml "gopkg.in/yaml.v3" + +// Unmarshal is just a wrapper of [gopkg.in/yaml.v3.Unmarshal]. +func Unmarshal(in []byte, out interface{}) error { + return goyaml.Unmarshal(in, out) +} diff --git a/vendor/github.com/stretchr/testify/assert/yaml/yaml_fail.go b/vendor/github.com/stretchr/testify/assert/yaml/yaml_fail.go new file mode 100644 index 000000000..e78f7dfe6 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/yaml/yaml_fail.go @@ -0,0 +1,18 @@ +//go:build testify_yaml_fail && !testify_yaml_custom && !testify_yaml_default +// +build testify_yaml_fail,!testify_yaml_custom,!testify_yaml_default + +// Package yaml is an implementation of YAML functions that always fail. +// +// This implementation can be used at build time to replace the default implementation +// to avoid linking with [gopkg.in/yaml.v3]: +// +// go test -tags testify_yaml_fail +package yaml + +import "errors" + +var errNotImplemented = errors.New("YAML functions are not available (see https://pkg.go.dev/github.com/stretchr/testify/assert/yaml)") + +func Unmarshal([]byte, interface{}) error { + return errNotImplemented +} diff --git a/vendor/github.com/stretchr/testify/require/require.go b/vendor/github.com/stretchr/testify/require/require.go index 506a82f80..d8921950d 100644 --- a/vendor/github.com/stretchr/testify/require/require.go +++ b/vendor/github.com/stretchr/testify/require/require.go @@ -34,9 +34,9 @@ func Conditionf(t TestingT, comp assert.Comparison, msg string, args ...interfac // Contains asserts that the specified string, list(array, slice...) or map contains the // specified substring or element. // -// assert.Contains(t, "Hello World", "World") -// assert.Contains(t, ["Hello", "World"], "World") -// assert.Contains(t, {"Hello": "World"}, "Hello") +// require.Contains(t, "Hello World", "World") +// require.Contains(t, ["Hello", "World"], "World") +// require.Contains(t, {"Hello": "World"}, "Hello") func Contains(t TestingT, s interface{}, contains interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -50,9 +50,9 @@ func Contains(t TestingT, s interface{}, contains interface{}, msgAndArgs ...int // Containsf asserts that the specified string, list(array, slice...) or map contains the // specified substring or element. // -// assert.Containsf(t, "Hello World", "World", "error message %s", "formatted") -// assert.Containsf(t, ["Hello", "World"], "World", "error message %s", "formatted") -// assert.Containsf(t, {"Hello": "World"}, "Hello", "error message %s", "formatted") +// require.Containsf(t, "Hello World", "World", "error message %s", "formatted") +// require.Containsf(t, ["Hello", "World"], "World", "error message %s", "formatted") +// require.Containsf(t, {"Hello": "World"}, "Hello", "error message %s", "formatted") func Containsf(t TestingT, s interface{}, contains interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -91,7 +91,7 @@ func DirExistsf(t TestingT, path string, msg string, args ...interface{}) { // listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, // the number of appearances of each of them in both lists should match. // -// assert.ElementsMatch(t, [1, 3, 2, 3], [1, 3, 3, 2]) +// require.ElementsMatch(t, [1, 3, 2, 3], [1, 3, 3, 2]) func ElementsMatch(t TestingT, listA interface{}, listB interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -106,7 +106,7 @@ func ElementsMatch(t TestingT, listA interface{}, listB interface{}, msgAndArgs // listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, // the number of appearances of each of them in both lists should match. // -// assert.ElementsMatchf(t, [1, 3, 2, 3], [1, 3, 3, 2], "error message %s", "formatted") +// require.ElementsMatchf(t, [1, 3, 2, 3], [1, 3, 3, 2], "error message %s", "formatted") func ElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -120,7 +120,7 @@ func ElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg string // Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either // a slice or a channel with len == 0. // -// assert.Empty(t, obj) +// require.Empty(t, obj) func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -134,7 +134,7 @@ func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) { // Emptyf asserts that the specified object is empty. I.e. nil, "", false, 0 or either // a slice or a channel with len == 0. // -// assert.Emptyf(t, obj, "error message %s", "formatted") +// require.Emptyf(t, obj, "error message %s", "formatted") func Emptyf(t TestingT, object interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -147,7 +147,7 @@ func Emptyf(t TestingT, object interface{}, msg string, args ...interface{}) { // Equal asserts that two objects are equal. // -// assert.Equal(t, 123, 123) +// require.Equal(t, 123, 123) // // Pointer variable equality is determined based on the equality of the // referenced values (as opposed to the memory addresses). Function equality @@ -166,7 +166,7 @@ func Equal(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...i // and that it is equal to the provided error. // // actualObj, err := SomeFunction() -// assert.EqualError(t, err, expectedErrorString) +// require.EqualError(t, err, expectedErrorString) func EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -181,7 +181,7 @@ func EqualError(t TestingT, theError error, errString string, msgAndArgs ...inte // and that it is equal to the provided error. // // actualObj, err := SomeFunction() -// assert.EqualErrorf(t, err, expectedErrorString, "error message %s", "formatted") +// require.EqualErrorf(t, err, expectedErrorString, "error message %s", "formatted") func EqualErrorf(t TestingT, theError error, errString string, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -200,8 +200,8 @@ func EqualErrorf(t TestingT, theError error, errString string, msg string, args // Exported int // notExported int // } -// assert.EqualExportedValues(t, S{1, 2}, S{1, 3}) => true -// assert.EqualExportedValues(t, S{1, 2}, S{2, 3}) => false +// require.EqualExportedValues(t, S{1, 2}, S{1, 3}) => true +// require.EqualExportedValues(t, S{1, 2}, S{2, 3}) => false func EqualExportedValues(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -220,8 +220,8 @@ func EqualExportedValues(t TestingT, expected interface{}, actual interface{}, m // Exported int // notExported int // } -// assert.EqualExportedValuesf(t, S{1, 2}, S{1, 3}, "error message %s", "formatted") => true -// assert.EqualExportedValuesf(t, S{1, 2}, S{2, 3}, "error message %s", "formatted") => false +// require.EqualExportedValuesf(t, S{1, 2}, S{1, 3}, "error message %s", "formatted") => true +// require.EqualExportedValuesf(t, S{1, 2}, S{2, 3}, "error message %s", "formatted") => false func EqualExportedValuesf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -232,10 +232,10 @@ func EqualExportedValuesf(t TestingT, expected interface{}, actual interface{}, t.FailNow() } -// EqualValues asserts that two objects are equal or convertible to the same types -// and equal. +// EqualValues asserts that two objects are equal or convertible to the larger +// type and equal. // -// assert.EqualValues(t, uint32(123), int32(123)) +// require.EqualValues(t, uint32(123), int32(123)) func EqualValues(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -246,10 +246,10 @@ func EqualValues(t TestingT, expected interface{}, actual interface{}, msgAndArg t.FailNow() } -// EqualValuesf asserts that two objects are equal or convertible to the same types -// and equal. +// EqualValuesf asserts that two objects are equal or convertible to the larger +// type and equal. // -// assert.EqualValuesf(t, uint32(123), int32(123), "error message %s", "formatted") +// require.EqualValuesf(t, uint32(123), int32(123), "error message %s", "formatted") func EqualValuesf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -262,7 +262,7 @@ func EqualValuesf(t TestingT, expected interface{}, actual interface{}, msg stri // Equalf asserts that two objects are equal. // -// assert.Equalf(t, 123, 123, "error message %s", "formatted") +// require.Equalf(t, 123, 123, "error message %s", "formatted") // // Pointer variable equality is determined based on the equality of the // referenced values (as opposed to the memory addresses). Function equality @@ -280,8 +280,8 @@ func Equalf(t TestingT, expected interface{}, actual interface{}, msg string, ar // Error asserts that a function returned an error (i.e. not `nil`). // // actualObj, err := SomeFunction() -// if assert.Error(t, err) { -// assert.Equal(t, expectedError, err) +// if require.Error(t, err) { +// require.Equal(t, expectedError, err) // } func Error(t TestingT, err error, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { @@ -321,7 +321,7 @@ func ErrorAsf(t TestingT, err error, target interface{}, msg string, args ...int // and that the error contains the specified substring. // // actualObj, err := SomeFunction() -// assert.ErrorContains(t, err, expectedErrorSubString) +// require.ErrorContains(t, err, expectedErrorSubString) func ErrorContains(t TestingT, theError error, contains string, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -336,7 +336,7 @@ func ErrorContains(t TestingT, theError error, contains string, msgAndArgs ...in // and that the error contains the specified substring. // // actualObj, err := SomeFunction() -// assert.ErrorContainsf(t, err, expectedErrorSubString, "error message %s", "formatted") +// require.ErrorContainsf(t, err, expectedErrorSubString, "error message %s", "formatted") func ErrorContainsf(t TestingT, theError error, contains string, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -374,8 +374,8 @@ func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface // Errorf asserts that a function returned an error (i.e. not `nil`). // // actualObj, err := SomeFunction() -// if assert.Errorf(t, err, "error message %s", "formatted") { -// assert.Equal(t, expectedErrorf, err) +// if require.Errorf(t, err, "error message %s", "formatted") { +// require.Equal(t, expectedErrorf, err) // } func Errorf(t TestingT, err error, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { @@ -390,7 +390,7 @@ func Errorf(t TestingT, err error, msg string, args ...interface{}) { // Eventually asserts that given condition will be met in waitFor time, // periodically checking target function each tick. // -// assert.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) +// require.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -415,10 +415,10 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t // time.Sleep(8*time.Second) // externalValue = true // }() -// assert.EventuallyWithT(t, func(c *assert.CollectT) { +// require.EventuallyWithT(t, func(c *require.CollectT) { // // add assertions as needed; any assertion failure will fail the current tick -// assert.True(c, externalValue, "expected 'externalValue' to be true") -// }, 1*time.Second, 10*time.Second, "external state has not changed to 'true'; still false") +// require.True(c, externalValue, "expected 'externalValue' to be true") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") func EventuallyWithT(t TestingT, condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -443,10 +443,10 @@ func EventuallyWithT(t TestingT, condition func(collect *assert.CollectT), waitF // time.Sleep(8*time.Second) // externalValue = true // }() -// assert.EventuallyWithTf(t, func(c *assert.CollectT, "error message %s", "formatted") { +// require.EventuallyWithTf(t, func(c *require.CollectT, "error message %s", "formatted") { // // add assertions as needed; any assertion failure will fail the current tick -// assert.True(c, externalValue, "expected 'externalValue' to be true") -// }, 1*time.Second, 10*time.Second, "external state has not changed to 'true'; still false") +// require.True(c, externalValue, "expected 'externalValue' to be true") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") func EventuallyWithTf(t TestingT, condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -460,7 +460,7 @@ func EventuallyWithTf(t TestingT, condition func(collect *assert.CollectT), wait // Eventuallyf asserts that given condition will be met in waitFor time, // periodically checking target function each tick. // -// assert.Eventuallyf(t, func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// require.Eventuallyf(t, func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -473,7 +473,7 @@ func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick // Exactly asserts that two objects are equal in value and type. // -// assert.Exactly(t, int32(123), int64(123)) +// require.Exactly(t, int32(123), int64(123)) func Exactly(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -486,7 +486,7 @@ func Exactly(t TestingT, expected interface{}, actual interface{}, msgAndArgs .. // Exactlyf asserts that two objects are equal in value and type. // -// assert.Exactlyf(t, int32(123), int64(123), "error message %s", "formatted") +// require.Exactlyf(t, int32(123), int64(123), "error message %s", "formatted") func Exactlyf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -543,7 +543,7 @@ func Failf(t TestingT, failureMessage string, msg string, args ...interface{}) { // False asserts that the specified value is false. // -// assert.False(t, myBool) +// require.False(t, myBool) func False(t TestingT, value bool, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -556,7 +556,7 @@ func False(t TestingT, value bool, msgAndArgs ...interface{}) { // Falsef asserts that the specified value is false. // -// assert.Falsef(t, myBool, "error message %s", "formatted") +// require.Falsef(t, myBool, "error message %s", "formatted") func Falsef(t TestingT, value bool, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -593,9 +593,9 @@ func FileExistsf(t TestingT, path string, msg string, args ...interface{}) { // Greater asserts that the first element is greater than the second // -// assert.Greater(t, 2, 1) -// assert.Greater(t, float64(2), float64(1)) -// assert.Greater(t, "b", "a") +// require.Greater(t, 2, 1) +// require.Greater(t, float64(2), float64(1)) +// require.Greater(t, "b", "a") func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -608,10 +608,10 @@ func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface // GreaterOrEqual asserts that the first element is greater than or equal to the second // -// assert.GreaterOrEqual(t, 2, 1) -// assert.GreaterOrEqual(t, 2, 2) -// assert.GreaterOrEqual(t, "b", "a") -// assert.GreaterOrEqual(t, "b", "b") +// require.GreaterOrEqual(t, 2, 1) +// require.GreaterOrEqual(t, 2, 2) +// require.GreaterOrEqual(t, "b", "a") +// require.GreaterOrEqual(t, "b", "b") func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -624,10 +624,10 @@ func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...in // GreaterOrEqualf asserts that the first element is greater than or equal to the second // -// assert.GreaterOrEqualf(t, 2, 1, "error message %s", "formatted") -// assert.GreaterOrEqualf(t, 2, 2, "error message %s", "formatted") -// assert.GreaterOrEqualf(t, "b", "a", "error message %s", "formatted") -// assert.GreaterOrEqualf(t, "b", "b", "error message %s", "formatted") +// require.GreaterOrEqualf(t, 2, 1, "error message %s", "formatted") +// require.GreaterOrEqualf(t, 2, 2, "error message %s", "formatted") +// require.GreaterOrEqualf(t, "b", "a", "error message %s", "formatted") +// require.GreaterOrEqualf(t, "b", "b", "error message %s", "formatted") func GreaterOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -640,9 +640,9 @@ func GreaterOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, arg // Greaterf asserts that the first element is greater than the second // -// assert.Greaterf(t, 2, 1, "error message %s", "formatted") -// assert.Greaterf(t, float64(2), float64(1), "error message %s", "formatted") -// assert.Greaterf(t, "b", "a", "error message %s", "formatted") +// require.Greaterf(t, 2, 1, "error message %s", "formatted") +// require.Greaterf(t, float64(2), float64(1), "error message %s", "formatted") +// require.Greaterf(t, "b", "a", "error message %s", "formatted") func Greaterf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -656,7 +656,7 @@ func Greaterf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...in // HTTPBodyContains asserts that a specified handler returns a // body that contains a string. // -// assert.HTTPBodyContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// require.HTTPBodyContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") // // Returns whether the assertion was successful (true) or not (false). func HTTPBodyContains(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { @@ -672,7 +672,7 @@ func HTTPBodyContains(t TestingT, handler http.HandlerFunc, method string, url s // HTTPBodyContainsf asserts that a specified handler returns a // body that contains a string. // -// assert.HTTPBodyContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// require.HTTPBodyContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") // // Returns whether the assertion was successful (true) or not (false). func HTTPBodyContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { @@ -688,7 +688,7 @@ func HTTPBodyContainsf(t TestingT, handler http.HandlerFunc, method string, url // HTTPBodyNotContains asserts that a specified handler returns a // body that does not contain a string. // -// assert.HTTPBodyNotContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// require.HTTPBodyNotContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") // // Returns whether the assertion was successful (true) or not (false). func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { @@ -704,7 +704,7 @@ func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, method string, ur // HTTPBodyNotContainsf asserts that a specified handler returns a // body that does not contain a string. // -// assert.HTTPBodyNotContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// require.HTTPBodyNotContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") // // Returns whether the assertion was successful (true) or not (false). func HTTPBodyNotContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { @@ -719,7 +719,7 @@ func HTTPBodyNotContainsf(t TestingT, handler http.HandlerFunc, method string, u // HTTPError asserts that a specified handler returns an error status code. // -// assert.HTTPError(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// require.HTTPError(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} // // Returns whether the assertion was successful (true) or not (false). func HTTPError(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { @@ -734,7 +734,7 @@ func HTTPError(t TestingT, handler http.HandlerFunc, method string, url string, // HTTPErrorf asserts that a specified handler returns an error status code. // -// assert.HTTPErrorf(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// require.HTTPErrorf(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} // // Returns whether the assertion was successful (true) or not (false). func HTTPErrorf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { @@ -749,7 +749,7 @@ func HTTPErrorf(t TestingT, handler http.HandlerFunc, method string, url string, // HTTPRedirect asserts that a specified handler returns a redirect status code. // -// assert.HTTPRedirect(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// require.HTTPRedirect(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} // // Returns whether the assertion was successful (true) or not (false). func HTTPRedirect(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { @@ -764,7 +764,7 @@ func HTTPRedirect(t TestingT, handler http.HandlerFunc, method string, url strin // HTTPRedirectf asserts that a specified handler returns a redirect status code. // -// assert.HTTPRedirectf(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// require.HTTPRedirectf(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} // // Returns whether the assertion was successful (true) or not (false). func HTTPRedirectf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { @@ -779,7 +779,7 @@ func HTTPRedirectf(t TestingT, handler http.HandlerFunc, method string, url stri // HTTPStatusCode asserts that a specified handler returns a specified status code. // -// assert.HTTPStatusCode(t, myHandler, "GET", "/notImplemented", nil, 501) +// require.HTTPStatusCode(t, myHandler, "GET", "/notImplemented", nil, 501) // // Returns whether the assertion was successful (true) or not (false). func HTTPStatusCode(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msgAndArgs ...interface{}) { @@ -794,7 +794,7 @@ func HTTPStatusCode(t TestingT, handler http.HandlerFunc, method string, url str // HTTPStatusCodef asserts that a specified handler returns a specified status code. // -// assert.HTTPStatusCodef(t, myHandler, "GET", "/notImplemented", nil, 501, "error message %s", "formatted") +// require.HTTPStatusCodef(t, myHandler, "GET", "/notImplemented", nil, 501, "error message %s", "formatted") // // Returns whether the assertion was successful (true) or not (false). func HTTPStatusCodef(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msg string, args ...interface{}) { @@ -809,7 +809,7 @@ func HTTPStatusCodef(t TestingT, handler http.HandlerFunc, method string, url st // HTTPSuccess asserts that a specified handler returns a success status code. // -// assert.HTTPSuccess(t, myHandler, "POST", "http://www.google.com", nil) +// require.HTTPSuccess(t, myHandler, "POST", "http://www.google.com", nil) // // Returns whether the assertion was successful (true) or not (false). func HTTPSuccess(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { @@ -824,7 +824,7 @@ func HTTPSuccess(t TestingT, handler http.HandlerFunc, method string, url string // HTTPSuccessf asserts that a specified handler returns a success status code. // -// assert.HTTPSuccessf(t, myHandler, "POST", "http://www.google.com", nil, "error message %s", "formatted") +// require.HTTPSuccessf(t, myHandler, "POST", "http://www.google.com", nil, "error message %s", "formatted") // // Returns whether the assertion was successful (true) or not (false). func HTTPSuccessf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { @@ -839,7 +839,7 @@ func HTTPSuccessf(t TestingT, handler http.HandlerFunc, method string, url strin // Implements asserts that an object is implemented by the specified interface. // -// assert.Implements(t, (*MyInterface)(nil), new(MyObject)) +// require.Implements(t, (*MyInterface)(nil), new(MyObject)) func Implements(t TestingT, interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -852,7 +852,7 @@ func Implements(t TestingT, interfaceObject interface{}, object interface{}, msg // Implementsf asserts that an object is implemented by the specified interface. // -// assert.Implementsf(t, (*MyInterface)(nil), new(MyObject), "error message %s", "formatted") +// require.Implementsf(t, (*MyInterface)(nil), new(MyObject), "error message %s", "formatted") func Implementsf(t TestingT, interfaceObject interface{}, object interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -865,7 +865,7 @@ func Implementsf(t TestingT, interfaceObject interface{}, object interface{}, ms // InDelta asserts that the two numerals are within delta of each other. // -// assert.InDelta(t, math.Pi, 22/7.0, 0.01) +// require.InDelta(t, math.Pi, 22/7.0, 0.01) func InDelta(t TestingT, expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -922,7 +922,7 @@ func InDeltaSlicef(t TestingT, expected interface{}, actual interface{}, delta f // InDeltaf asserts that the two numerals are within delta of each other. // -// assert.InDeltaf(t, math.Pi, 22/7.0, 0.01, "error message %s", "formatted") +// require.InDeltaf(t, math.Pi, 22/7.0, 0.01, "error message %s", "formatted") func InDeltaf(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -979,9 +979,9 @@ func InEpsilonf(t TestingT, expected interface{}, actual interface{}, epsilon fl // IsDecreasing asserts that the collection is decreasing // -// assert.IsDecreasing(t, []int{2, 1, 0}) -// assert.IsDecreasing(t, []float{2, 1}) -// assert.IsDecreasing(t, []string{"b", "a"}) +// require.IsDecreasing(t, []int{2, 1, 0}) +// require.IsDecreasing(t, []float{2, 1}) +// require.IsDecreasing(t, []string{"b", "a"}) func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -994,9 +994,9 @@ func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) { // IsDecreasingf asserts that the collection is decreasing // -// assert.IsDecreasingf(t, []int{2, 1, 0}, "error message %s", "formatted") -// assert.IsDecreasingf(t, []float{2, 1}, "error message %s", "formatted") -// assert.IsDecreasingf(t, []string{"b", "a"}, "error message %s", "formatted") +// require.IsDecreasingf(t, []int{2, 1, 0}, "error message %s", "formatted") +// require.IsDecreasingf(t, []float{2, 1}, "error message %s", "formatted") +// require.IsDecreasingf(t, []string{"b", "a"}, "error message %s", "formatted") func IsDecreasingf(t TestingT, object interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1009,9 +1009,9 @@ func IsDecreasingf(t TestingT, object interface{}, msg string, args ...interface // IsIncreasing asserts that the collection is increasing // -// assert.IsIncreasing(t, []int{1, 2, 3}) -// assert.IsIncreasing(t, []float{1, 2}) -// assert.IsIncreasing(t, []string{"a", "b"}) +// require.IsIncreasing(t, []int{1, 2, 3}) +// require.IsIncreasing(t, []float{1, 2}) +// require.IsIncreasing(t, []string{"a", "b"}) func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1024,9 +1024,9 @@ func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) { // IsIncreasingf asserts that the collection is increasing // -// assert.IsIncreasingf(t, []int{1, 2, 3}, "error message %s", "formatted") -// assert.IsIncreasingf(t, []float{1, 2}, "error message %s", "formatted") -// assert.IsIncreasingf(t, []string{"a", "b"}, "error message %s", "formatted") +// require.IsIncreasingf(t, []int{1, 2, 3}, "error message %s", "formatted") +// require.IsIncreasingf(t, []float{1, 2}, "error message %s", "formatted") +// require.IsIncreasingf(t, []string{"a", "b"}, "error message %s", "formatted") func IsIncreasingf(t TestingT, object interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1039,9 +1039,9 @@ func IsIncreasingf(t TestingT, object interface{}, msg string, args ...interface // IsNonDecreasing asserts that the collection is not decreasing // -// assert.IsNonDecreasing(t, []int{1, 1, 2}) -// assert.IsNonDecreasing(t, []float{1, 2}) -// assert.IsNonDecreasing(t, []string{"a", "b"}) +// require.IsNonDecreasing(t, []int{1, 1, 2}) +// require.IsNonDecreasing(t, []float{1, 2}) +// require.IsNonDecreasing(t, []string{"a", "b"}) func IsNonDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1054,9 +1054,9 @@ func IsNonDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) // IsNonDecreasingf asserts that the collection is not decreasing // -// assert.IsNonDecreasingf(t, []int{1, 1, 2}, "error message %s", "formatted") -// assert.IsNonDecreasingf(t, []float{1, 2}, "error message %s", "formatted") -// assert.IsNonDecreasingf(t, []string{"a", "b"}, "error message %s", "formatted") +// require.IsNonDecreasingf(t, []int{1, 1, 2}, "error message %s", "formatted") +// require.IsNonDecreasingf(t, []float{1, 2}, "error message %s", "formatted") +// require.IsNonDecreasingf(t, []string{"a", "b"}, "error message %s", "formatted") func IsNonDecreasingf(t TestingT, object interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1069,9 +1069,9 @@ func IsNonDecreasingf(t TestingT, object interface{}, msg string, args ...interf // IsNonIncreasing asserts that the collection is not increasing // -// assert.IsNonIncreasing(t, []int{2, 1, 1}) -// assert.IsNonIncreasing(t, []float{2, 1}) -// assert.IsNonIncreasing(t, []string{"b", "a"}) +// require.IsNonIncreasing(t, []int{2, 1, 1}) +// require.IsNonIncreasing(t, []float{2, 1}) +// require.IsNonIncreasing(t, []string{"b", "a"}) func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1084,9 +1084,9 @@ func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) // IsNonIncreasingf asserts that the collection is not increasing // -// assert.IsNonIncreasingf(t, []int{2, 1, 1}, "error message %s", "formatted") -// assert.IsNonIncreasingf(t, []float{2, 1}, "error message %s", "formatted") -// assert.IsNonIncreasingf(t, []string{"b", "a"}, "error message %s", "formatted") +// require.IsNonIncreasingf(t, []int{2, 1, 1}, "error message %s", "formatted") +// require.IsNonIncreasingf(t, []float{2, 1}, "error message %s", "formatted") +// require.IsNonIncreasingf(t, []string{"b", "a"}, "error message %s", "formatted") func IsNonIncreasingf(t TestingT, object interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1121,7 +1121,7 @@ func IsTypef(t TestingT, expectedType interface{}, object interface{}, msg strin // JSONEq asserts that two JSON strings are equivalent. // -// assert.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +// require.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) func JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1134,7 +1134,7 @@ func JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{ // JSONEqf asserts that two JSON strings are equivalent. // -// assert.JSONEqf(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`, "error message %s", "formatted") +// require.JSONEqf(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`, "error message %s", "formatted") func JSONEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1148,7 +1148,7 @@ func JSONEqf(t TestingT, expected string, actual string, msg string, args ...int // Len asserts that the specified object has specific length. // Len also fails if the object has a type that len() not accept. // -// assert.Len(t, mySlice, 3) +// require.Len(t, mySlice, 3) func Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1162,7 +1162,7 @@ func Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) // Lenf asserts that the specified object has specific length. // Lenf also fails if the object has a type that len() not accept. // -// assert.Lenf(t, mySlice, 3, "error message %s", "formatted") +// require.Lenf(t, mySlice, 3, "error message %s", "formatted") func Lenf(t TestingT, object interface{}, length int, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1175,9 +1175,9 @@ func Lenf(t TestingT, object interface{}, length int, msg string, args ...interf // Less asserts that the first element is less than the second // -// assert.Less(t, 1, 2) -// assert.Less(t, float64(1), float64(2)) -// assert.Less(t, "a", "b") +// require.Less(t, 1, 2) +// require.Less(t, float64(1), float64(2)) +// require.Less(t, "a", "b") func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1190,10 +1190,10 @@ func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) // LessOrEqual asserts that the first element is less than or equal to the second // -// assert.LessOrEqual(t, 1, 2) -// assert.LessOrEqual(t, 2, 2) -// assert.LessOrEqual(t, "a", "b") -// assert.LessOrEqual(t, "b", "b") +// require.LessOrEqual(t, 1, 2) +// require.LessOrEqual(t, 2, 2) +// require.LessOrEqual(t, "a", "b") +// require.LessOrEqual(t, "b", "b") func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1206,10 +1206,10 @@ func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...inter // LessOrEqualf asserts that the first element is less than or equal to the second // -// assert.LessOrEqualf(t, 1, 2, "error message %s", "formatted") -// assert.LessOrEqualf(t, 2, 2, "error message %s", "formatted") -// assert.LessOrEqualf(t, "a", "b", "error message %s", "formatted") -// assert.LessOrEqualf(t, "b", "b", "error message %s", "formatted") +// require.LessOrEqualf(t, 1, 2, "error message %s", "formatted") +// require.LessOrEqualf(t, 2, 2, "error message %s", "formatted") +// require.LessOrEqualf(t, "a", "b", "error message %s", "formatted") +// require.LessOrEqualf(t, "b", "b", "error message %s", "formatted") func LessOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1222,9 +1222,9 @@ func LessOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, args . // Lessf asserts that the first element is less than the second // -// assert.Lessf(t, 1, 2, "error message %s", "formatted") -// assert.Lessf(t, float64(1), float64(2), "error message %s", "formatted") -// assert.Lessf(t, "a", "b", "error message %s", "formatted") +// require.Lessf(t, 1, 2, "error message %s", "formatted") +// require.Lessf(t, float64(1), float64(2), "error message %s", "formatted") +// require.Lessf(t, "a", "b", "error message %s", "formatted") func Lessf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1237,8 +1237,8 @@ func Lessf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...inter // Negative asserts that the specified element is negative // -// assert.Negative(t, -1) -// assert.Negative(t, -1.23) +// require.Negative(t, -1) +// require.Negative(t, -1.23) func Negative(t TestingT, e interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1251,8 +1251,8 @@ func Negative(t TestingT, e interface{}, msgAndArgs ...interface{}) { // Negativef asserts that the specified element is negative // -// assert.Negativef(t, -1, "error message %s", "formatted") -// assert.Negativef(t, -1.23, "error message %s", "formatted") +// require.Negativef(t, -1, "error message %s", "formatted") +// require.Negativef(t, -1.23, "error message %s", "formatted") func Negativef(t TestingT, e interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1266,7 +1266,7 @@ func Negativef(t TestingT, e interface{}, msg string, args ...interface{}) { // Never asserts that the given condition doesn't satisfy in waitFor time, // periodically checking the target function each tick. // -// assert.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond) +// require.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond) func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1280,7 +1280,7 @@ func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.D // Neverf asserts that the given condition doesn't satisfy in waitFor time, // periodically checking the target function each tick. // -// assert.Neverf(t, func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// require.Neverf(t, func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") func Neverf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1293,7 +1293,7 @@ func Neverf(t TestingT, condition func() bool, waitFor time.Duration, tick time. // Nil asserts that the specified object is nil. // -// assert.Nil(t, err) +// require.Nil(t, err) func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1306,7 +1306,7 @@ func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) { // Nilf asserts that the specified object is nil. // -// assert.Nilf(t, err, "error message %s", "formatted") +// require.Nilf(t, err, "error message %s", "formatted") func Nilf(t TestingT, object interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1344,8 +1344,8 @@ func NoDirExistsf(t TestingT, path string, msg string, args ...interface{}) { // NoError asserts that a function returned no error (i.e. `nil`). // // actualObj, err := SomeFunction() -// if assert.NoError(t, err) { -// assert.Equal(t, expectedObj, actualObj) +// if require.NoError(t, err) { +// require.Equal(t, expectedObj, actualObj) // } func NoError(t TestingT, err error, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { @@ -1360,8 +1360,8 @@ func NoError(t TestingT, err error, msgAndArgs ...interface{}) { // NoErrorf asserts that a function returned no error (i.e. `nil`). // // actualObj, err := SomeFunction() -// if assert.NoErrorf(t, err, "error message %s", "formatted") { -// assert.Equal(t, expectedObj, actualObj) +// if require.NoErrorf(t, err, "error message %s", "formatted") { +// require.Equal(t, expectedObj, actualObj) // } func NoErrorf(t TestingT, err error, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { @@ -1400,9 +1400,9 @@ func NoFileExistsf(t TestingT, path string, msg string, args ...interface{}) { // NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the // specified substring or element. // -// assert.NotContains(t, "Hello World", "Earth") -// assert.NotContains(t, ["Hello", "World"], "Earth") -// assert.NotContains(t, {"Hello": "World"}, "Earth") +// require.NotContains(t, "Hello World", "Earth") +// require.NotContains(t, ["Hello", "World"], "Earth") +// require.NotContains(t, {"Hello": "World"}, "Earth") func NotContains(t TestingT, s interface{}, contains interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1416,9 +1416,9 @@ func NotContains(t TestingT, s interface{}, contains interface{}, msgAndArgs ... // NotContainsf asserts that the specified string, list(array, slice...) or map does NOT contain the // specified substring or element. // -// assert.NotContainsf(t, "Hello World", "Earth", "error message %s", "formatted") -// assert.NotContainsf(t, ["Hello", "World"], "Earth", "error message %s", "formatted") -// assert.NotContainsf(t, {"Hello": "World"}, "Earth", "error message %s", "formatted") +// require.NotContainsf(t, "Hello World", "Earth", "error message %s", "formatted") +// require.NotContainsf(t, ["Hello", "World"], "Earth", "error message %s", "formatted") +// require.NotContainsf(t, {"Hello": "World"}, "Earth", "error message %s", "formatted") func NotContainsf(t TestingT, s interface{}, contains interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1429,11 +1429,51 @@ func NotContainsf(t TestingT, s interface{}, contains interface{}, msg string, a t.FailNow() } +// NotElementsMatch asserts that the specified listA(array, slice...) is NOT equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should not match. +// This is an inverse of ElementsMatch. +// +// require.NotElementsMatch(t, [1, 1, 2, 3], [1, 1, 2, 3]) -> false +// +// require.NotElementsMatch(t, [1, 1, 2, 3], [1, 2, 3]) -> true +// +// require.NotElementsMatch(t, [1, 2, 3], [1, 2, 4]) -> true +func NotElementsMatch(t TestingT, listA interface{}, listB interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotElementsMatch(t, listA, listB, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotElementsMatchf asserts that the specified listA(array, slice...) is NOT equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should not match. +// This is an inverse of ElementsMatch. +// +// require.NotElementsMatchf(t, [1, 1, 2, 3], [1, 1, 2, 3], "error message %s", "formatted") -> false +// +// require.NotElementsMatchf(t, [1, 1, 2, 3], [1, 2, 3], "error message %s", "formatted") -> true +// +// require.NotElementsMatchf(t, [1, 2, 3], [1, 2, 4], "error message %s", "formatted") -> true +func NotElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotElementsMatchf(t, listA, listB, msg, args...) { + return + } + t.FailNow() +} + // NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either // a slice or a channel with len == 0. // -// if assert.NotEmpty(t, obj) { -// assert.Equal(t, "two", obj[1]) +// if require.NotEmpty(t, obj) { +// require.Equal(t, "two", obj[1]) // } func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { @@ -1448,8 +1488,8 @@ func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) { // NotEmptyf asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either // a slice or a channel with len == 0. // -// if assert.NotEmptyf(t, obj, "error message %s", "formatted") { -// assert.Equal(t, "two", obj[1]) +// if require.NotEmptyf(t, obj, "error message %s", "formatted") { +// require.Equal(t, "two", obj[1]) // } func NotEmptyf(t TestingT, object interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { @@ -1463,7 +1503,7 @@ func NotEmptyf(t TestingT, object interface{}, msg string, args ...interface{}) // NotEqual asserts that the specified values are NOT equal. // -// assert.NotEqual(t, obj1, obj2) +// require.NotEqual(t, obj1, obj2) // // Pointer variable equality is determined based on the equality of the // referenced values (as opposed to the memory addresses). @@ -1479,7 +1519,7 @@ func NotEqual(t TestingT, expected interface{}, actual interface{}, msgAndArgs . // NotEqualValues asserts that two objects are not equal even when converted to the same type // -// assert.NotEqualValues(t, obj1, obj2) +// require.NotEqualValues(t, obj1, obj2) func NotEqualValues(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1492,7 +1532,7 @@ func NotEqualValues(t TestingT, expected interface{}, actual interface{}, msgAnd // NotEqualValuesf asserts that two objects are not equal even when converted to the same type // -// assert.NotEqualValuesf(t, obj1, obj2, "error message %s", "formatted") +// require.NotEqualValuesf(t, obj1, obj2, "error message %s", "formatted") func NotEqualValuesf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1505,7 +1545,7 @@ func NotEqualValuesf(t TestingT, expected interface{}, actual interface{}, msg s // NotEqualf asserts that the specified values are NOT equal. // -// assert.NotEqualf(t, obj1, obj2, "error message %s", "formatted") +// require.NotEqualf(t, obj1, obj2, "error message %s", "formatted") // // Pointer variable equality is determined based on the equality of the // referenced values (as opposed to the memory addresses). @@ -1519,7 +1559,31 @@ func NotEqualf(t TestingT, expected interface{}, actual interface{}, msg string, t.FailNow() } -// NotErrorIs asserts that at none of the errors in err's chain matches target. +// NotErrorAs asserts that none of the errors in err's chain matches target, +// but if so, sets target to that error value. +func NotErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotErrorAs(t, err, target, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotErrorAsf asserts that none of the errors in err's chain matches target, +// but if so, sets target to that error value. +func NotErrorAsf(t TestingT, err error, target interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotErrorAsf(t, err, target, msg, args...) { + return + } + t.FailNow() +} + +// NotErrorIs asserts that none of the errors in err's chain matches target. // This is a wrapper for errors.Is. func NotErrorIs(t TestingT, err error, target error, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { @@ -1531,7 +1595,7 @@ func NotErrorIs(t TestingT, err error, target error, msgAndArgs ...interface{}) t.FailNow() } -// NotErrorIsf asserts that at none of the errors in err's chain matches target. +// NotErrorIsf asserts that none of the errors in err's chain matches target. // This is a wrapper for errors.Is. func NotErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { @@ -1545,7 +1609,7 @@ func NotErrorIsf(t TestingT, err error, target error, msg string, args ...interf // NotImplements asserts that an object does not implement the specified interface. // -// assert.NotImplements(t, (*MyInterface)(nil), new(MyObject)) +// require.NotImplements(t, (*MyInterface)(nil), new(MyObject)) func NotImplements(t TestingT, interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1558,7 +1622,7 @@ func NotImplements(t TestingT, interfaceObject interface{}, object interface{}, // NotImplementsf asserts that an object does not implement the specified interface. // -// assert.NotImplementsf(t, (*MyInterface)(nil), new(MyObject), "error message %s", "formatted") +// require.NotImplementsf(t, (*MyInterface)(nil), new(MyObject), "error message %s", "formatted") func NotImplementsf(t TestingT, interfaceObject interface{}, object interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1571,7 +1635,7 @@ func NotImplementsf(t TestingT, interfaceObject interface{}, object interface{}, // NotNil asserts that the specified object is not nil. // -// assert.NotNil(t, err) +// require.NotNil(t, err) func NotNil(t TestingT, object interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1584,7 +1648,7 @@ func NotNil(t TestingT, object interface{}, msgAndArgs ...interface{}) { // NotNilf asserts that the specified object is not nil. // -// assert.NotNilf(t, err, "error message %s", "formatted") +// require.NotNilf(t, err, "error message %s", "formatted") func NotNilf(t TestingT, object interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1597,7 +1661,7 @@ func NotNilf(t TestingT, object interface{}, msg string, args ...interface{}) { // NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. // -// assert.NotPanics(t, func(){ RemainCalm() }) +// require.NotPanics(t, func(){ RemainCalm() }) func NotPanics(t TestingT, f assert.PanicTestFunc, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1610,7 +1674,7 @@ func NotPanics(t TestingT, f assert.PanicTestFunc, msgAndArgs ...interface{}) { // NotPanicsf asserts that the code inside the specified PanicTestFunc does NOT panic. // -// assert.NotPanicsf(t, func(){ RemainCalm() }, "error message %s", "formatted") +// require.NotPanicsf(t, func(){ RemainCalm() }, "error message %s", "formatted") func NotPanicsf(t TestingT, f assert.PanicTestFunc, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1623,8 +1687,8 @@ func NotPanicsf(t TestingT, f assert.PanicTestFunc, msg string, args ...interfac // NotRegexp asserts that a specified regexp does not match a string. // -// assert.NotRegexp(t, regexp.MustCompile("starts"), "it's starting") -// assert.NotRegexp(t, "^start", "it's not starting") +// require.NotRegexp(t, regexp.MustCompile("starts"), "it's starting") +// require.NotRegexp(t, "^start", "it's not starting") func NotRegexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1637,8 +1701,8 @@ func NotRegexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interf // NotRegexpf asserts that a specified regexp does not match a string. // -// assert.NotRegexpf(t, regexp.MustCompile("starts"), "it's starting", "error message %s", "formatted") -// assert.NotRegexpf(t, "^start", "it's not starting", "error message %s", "formatted") +// require.NotRegexpf(t, regexp.MustCompile("starts"), "it's starting", "error message %s", "formatted") +// require.NotRegexpf(t, "^start", "it's not starting", "error message %s", "formatted") func NotRegexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1651,7 +1715,7 @@ func NotRegexpf(t TestingT, rx interface{}, str interface{}, msg string, args .. // NotSame asserts that two pointers do not reference the same object. // -// assert.NotSame(t, ptr1, ptr2) +// require.NotSame(t, ptr1, ptr2) // // Both arguments must be pointer variables. Pointer variable sameness is // determined based on the equality of both type and value. @@ -1667,7 +1731,7 @@ func NotSame(t TestingT, expected interface{}, actual interface{}, msgAndArgs .. // NotSamef asserts that two pointers do not reference the same object. // -// assert.NotSamef(t, ptr1, ptr2, "error message %s", "formatted") +// require.NotSamef(t, ptr1, ptr2, "error message %s", "formatted") // // Both arguments must be pointer variables. Pointer variable sameness is // determined based on the equality of both type and value. @@ -1685,8 +1749,8 @@ func NotSamef(t TestingT, expected interface{}, actual interface{}, msg string, // contain all elements given in the specified subset list(array, slice...) or // map. // -// assert.NotSubset(t, [1, 3, 4], [1, 2]) -// assert.NotSubset(t, {"x": 1, "y": 2}, {"z": 3}) +// require.NotSubset(t, [1, 3, 4], [1, 2]) +// require.NotSubset(t, {"x": 1, "y": 2}, {"z": 3}) func NotSubset(t TestingT, list interface{}, subset interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1701,8 +1765,8 @@ func NotSubset(t TestingT, list interface{}, subset interface{}, msgAndArgs ...i // contain all elements given in the specified subset list(array, slice...) or // map. // -// assert.NotSubsetf(t, [1, 3, 4], [1, 2], "error message %s", "formatted") -// assert.NotSubsetf(t, {"x": 1, "y": 2}, {"z": 3}, "error message %s", "formatted") +// require.NotSubsetf(t, [1, 3, 4], [1, 2], "error message %s", "formatted") +// require.NotSubsetf(t, {"x": 1, "y": 2}, {"z": 3}, "error message %s", "formatted") func NotSubsetf(t TestingT, list interface{}, subset interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1737,7 +1801,7 @@ func NotZerof(t TestingT, i interface{}, msg string, args ...interface{}) { // Panics asserts that the code inside the specified PanicTestFunc panics. // -// assert.Panics(t, func(){ GoCrazy() }) +// require.Panics(t, func(){ GoCrazy() }) func Panics(t TestingT, f assert.PanicTestFunc, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1752,7 +1816,7 @@ func Panics(t TestingT, f assert.PanicTestFunc, msgAndArgs ...interface{}) { // panics, and that the recovered panic value is an error that satisfies the // EqualError comparison. // -// assert.PanicsWithError(t, "crazy error", func(){ GoCrazy() }) +// require.PanicsWithError(t, "crazy error", func(){ GoCrazy() }) func PanicsWithError(t TestingT, errString string, f assert.PanicTestFunc, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1767,7 +1831,7 @@ func PanicsWithError(t TestingT, errString string, f assert.PanicTestFunc, msgAn // panics, and that the recovered panic value is an error that satisfies the // EqualError comparison. // -// assert.PanicsWithErrorf(t, "crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +// require.PanicsWithErrorf(t, "crazy error", func(){ GoCrazy() }, "error message %s", "formatted") func PanicsWithErrorf(t TestingT, errString string, f assert.PanicTestFunc, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1781,7 +1845,7 @@ func PanicsWithErrorf(t TestingT, errString string, f assert.PanicTestFunc, msg // PanicsWithValue asserts that the code inside the specified PanicTestFunc panics, and that // the recovered panic value equals the expected panic value. // -// assert.PanicsWithValue(t, "crazy error", func(){ GoCrazy() }) +// require.PanicsWithValue(t, "crazy error", func(){ GoCrazy() }) func PanicsWithValue(t TestingT, expected interface{}, f assert.PanicTestFunc, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1795,7 +1859,7 @@ func PanicsWithValue(t TestingT, expected interface{}, f assert.PanicTestFunc, m // PanicsWithValuef asserts that the code inside the specified PanicTestFunc panics, and that // the recovered panic value equals the expected panic value. // -// assert.PanicsWithValuef(t, "crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +// require.PanicsWithValuef(t, "crazy error", func(){ GoCrazy() }, "error message %s", "formatted") func PanicsWithValuef(t TestingT, expected interface{}, f assert.PanicTestFunc, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1808,7 +1872,7 @@ func PanicsWithValuef(t TestingT, expected interface{}, f assert.PanicTestFunc, // Panicsf asserts that the code inside the specified PanicTestFunc panics. // -// assert.Panicsf(t, func(){ GoCrazy() }, "error message %s", "formatted") +// require.Panicsf(t, func(){ GoCrazy() }, "error message %s", "formatted") func Panicsf(t TestingT, f assert.PanicTestFunc, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1821,8 +1885,8 @@ func Panicsf(t TestingT, f assert.PanicTestFunc, msg string, args ...interface{} // Positive asserts that the specified element is positive // -// assert.Positive(t, 1) -// assert.Positive(t, 1.23) +// require.Positive(t, 1) +// require.Positive(t, 1.23) func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1835,8 +1899,8 @@ func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) { // Positivef asserts that the specified element is positive // -// assert.Positivef(t, 1, "error message %s", "formatted") -// assert.Positivef(t, 1.23, "error message %s", "formatted") +// require.Positivef(t, 1, "error message %s", "formatted") +// require.Positivef(t, 1.23, "error message %s", "formatted") func Positivef(t TestingT, e interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1849,8 +1913,8 @@ func Positivef(t TestingT, e interface{}, msg string, args ...interface{}) { // Regexp asserts that a specified regexp matches a string. // -// assert.Regexp(t, regexp.MustCompile("start"), "it's starting") -// assert.Regexp(t, "start...$", "it's not starting") +// require.Regexp(t, regexp.MustCompile("start"), "it's starting") +// require.Regexp(t, "start...$", "it's not starting") func Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1863,8 +1927,8 @@ func Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface // Regexpf asserts that a specified regexp matches a string. // -// assert.Regexpf(t, regexp.MustCompile("start"), "it's starting", "error message %s", "formatted") -// assert.Regexpf(t, "start...$", "it's not starting", "error message %s", "formatted") +// require.Regexpf(t, regexp.MustCompile("start"), "it's starting", "error message %s", "formatted") +// require.Regexpf(t, "start...$", "it's not starting", "error message %s", "formatted") func Regexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1877,7 +1941,7 @@ func Regexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...in // Same asserts that two pointers reference the same object. // -// assert.Same(t, ptr1, ptr2) +// require.Same(t, ptr1, ptr2) // // Both arguments must be pointer variables. Pointer variable sameness is // determined based on the equality of both type and value. @@ -1893,7 +1957,7 @@ func Same(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...in // Samef asserts that two pointers reference the same object. // -// assert.Samef(t, ptr1, ptr2, "error message %s", "formatted") +// require.Samef(t, ptr1, ptr2, "error message %s", "formatted") // // Both arguments must be pointer variables. Pointer variable sameness is // determined based on the equality of both type and value. @@ -1910,8 +1974,8 @@ func Samef(t TestingT, expected interface{}, actual interface{}, msg string, arg // Subset asserts that the specified list(array, slice...) or map contains all // elements given in the specified subset list(array, slice...) or map. // -// assert.Subset(t, [1, 2, 3], [1, 2]) -// assert.Subset(t, {"x": 1, "y": 2}, {"x": 1}) +// require.Subset(t, [1, 2, 3], [1, 2]) +// require.Subset(t, {"x": 1, "y": 2}, {"x": 1}) func Subset(t TestingT, list interface{}, subset interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1925,8 +1989,8 @@ func Subset(t TestingT, list interface{}, subset interface{}, msgAndArgs ...inte // Subsetf asserts that the specified list(array, slice...) or map contains all // elements given in the specified subset list(array, slice...) or map. // -// assert.Subsetf(t, [1, 2, 3], [1, 2], "error message %s", "formatted") -// assert.Subsetf(t, {"x": 1, "y": 2}, {"x": 1}, "error message %s", "formatted") +// require.Subsetf(t, [1, 2, 3], [1, 2], "error message %s", "formatted") +// require.Subsetf(t, {"x": 1, "y": 2}, {"x": 1}, "error message %s", "formatted") func Subsetf(t TestingT, list interface{}, subset interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1939,7 +2003,7 @@ func Subsetf(t TestingT, list interface{}, subset interface{}, msg string, args // True asserts that the specified value is true. // -// assert.True(t, myBool) +// require.True(t, myBool) func True(t TestingT, value bool, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1952,7 +2016,7 @@ func True(t TestingT, value bool, msgAndArgs ...interface{}) { // Truef asserts that the specified value is true. // -// assert.Truef(t, myBool, "error message %s", "formatted") +// require.Truef(t, myBool, "error message %s", "formatted") func Truef(t TestingT, value bool, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1965,7 +2029,7 @@ func Truef(t TestingT, value bool, msg string, args ...interface{}) { // WithinDuration asserts that the two times are within duration delta of each other. // -// assert.WithinDuration(t, time.Now(), time.Now(), 10*time.Second) +// require.WithinDuration(t, time.Now(), time.Now(), 10*time.Second) func WithinDuration(t TestingT, expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1978,7 +2042,7 @@ func WithinDuration(t TestingT, expected time.Time, actual time.Time, delta time // WithinDurationf asserts that the two times are within duration delta of each other. // -// assert.WithinDurationf(t, time.Now(), time.Now(), 10*time.Second, "error message %s", "formatted") +// require.WithinDurationf(t, time.Now(), time.Now(), 10*time.Second, "error message %s", "formatted") func WithinDurationf(t TestingT, expected time.Time, actual time.Time, delta time.Duration, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1991,7 +2055,7 @@ func WithinDurationf(t TestingT, expected time.Time, actual time.Time, delta tim // WithinRange asserts that a time is within a time range (inclusive). // -// assert.WithinRange(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second)) +// require.WithinRange(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second)) func WithinRange(t TestingT, actual time.Time, start time.Time, end time.Time, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -2004,7 +2068,7 @@ func WithinRange(t TestingT, actual time.Time, start time.Time, end time.Time, m // WithinRangef asserts that a time is within a time range (inclusive). // -// assert.WithinRangef(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second), "error message %s", "formatted") +// require.WithinRangef(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second), "error message %s", "formatted") func WithinRangef(t TestingT, actual time.Time, start time.Time, end time.Time, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() diff --git a/vendor/github.com/stretchr/testify/require/require.go.tmpl b/vendor/github.com/stretchr/testify/require/require.go.tmpl index 55e42ddeb..8b3283685 100644 --- a/vendor/github.com/stretchr/testify/require/require.go.tmpl +++ b/vendor/github.com/stretchr/testify/require/require.go.tmpl @@ -1,4 +1,4 @@ -{{.Comment}} +{{ replace .Comment "assert." "require."}} func {{.DocInfo.Name}}(t TestingT, {{.Params}}) { if h, ok := t.(tHelper); ok { h.Helper() } if assert.{{.DocInfo.Name}}(t, {{.ForwardedParams}}) { return } diff --git a/vendor/github.com/stretchr/testify/require/require_forward.go b/vendor/github.com/stretchr/testify/require/require_forward.go index eee8310a5..1bd87304f 100644 --- a/vendor/github.com/stretchr/testify/require/require_forward.go +++ b/vendor/github.com/stretchr/testify/require/require_forward.go @@ -187,8 +187,8 @@ func (a *Assertions) EqualExportedValuesf(expected interface{}, actual interface EqualExportedValuesf(a.t, expected, actual, msg, args...) } -// EqualValues asserts that two objects are equal or convertible to the same types -// and equal. +// EqualValues asserts that two objects are equal or convertible to the larger +// type and equal. // // a.EqualValues(uint32(123), int32(123)) func (a *Assertions) EqualValues(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { @@ -198,8 +198,8 @@ func (a *Assertions) EqualValues(expected interface{}, actual interface{}, msgAn EqualValues(a.t, expected, actual, msgAndArgs...) } -// EqualValuesf asserts that two objects are equal or convertible to the same types -// and equal. +// EqualValuesf asserts that two objects are equal or convertible to the larger +// type and equal. // // a.EqualValuesf(uint32(123), int32(123), "error message %s", "formatted") func (a *Assertions) EqualValuesf(expected interface{}, actual interface{}, msg string, args ...interface{}) { @@ -337,7 +337,7 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti // a.EventuallyWithT(func(c *assert.CollectT) { // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") -// }, 1*time.Second, 10*time.Second, "external state has not changed to 'true'; still false") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") func (a *Assertions) EventuallyWithT(condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -362,7 +362,7 @@ func (a *Assertions) EventuallyWithT(condition func(collect *assert.CollectT), w // a.EventuallyWithTf(func(c *assert.CollectT, "error message %s", "formatted") { // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") -// }, 1*time.Second, 10*time.Second, "external state has not changed to 'true'; still false") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") func (a *Assertions) EventuallyWithTf(condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1129,6 +1129,40 @@ func (a *Assertions) NotContainsf(s interface{}, contains interface{}, msg strin NotContainsf(a.t, s, contains, msg, args...) } +// NotElementsMatch asserts that the specified listA(array, slice...) is NOT equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should not match. +// This is an inverse of ElementsMatch. +// +// a.NotElementsMatch([1, 1, 2, 3], [1, 1, 2, 3]) -> false +// +// a.NotElementsMatch([1, 1, 2, 3], [1, 2, 3]) -> true +// +// a.NotElementsMatch([1, 2, 3], [1, 2, 4]) -> true +func (a *Assertions) NotElementsMatch(listA interface{}, listB interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotElementsMatch(a.t, listA, listB, msgAndArgs...) +} + +// NotElementsMatchf asserts that the specified listA(array, slice...) is NOT equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should not match. +// This is an inverse of ElementsMatch. +// +// a.NotElementsMatchf([1, 1, 2, 3], [1, 1, 2, 3], "error message %s", "formatted") -> false +// +// a.NotElementsMatchf([1, 1, 2, 3], [1, 2, 3], "error message %s", "formatted") -> true +// +// a.NotElementsMatchf([1, 2, 3], [1, 2, 4], "error message %s", "formatted") -> true +func (a *Assertions) NotElementsMatchf(listA interface{}, listB interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotElementsMatchf(a.t, listA, listB, msg, args...) +} + // NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either // a slice or a channel with len == 0. // @@ -1201,7 +1235,25 @@ func (a *Assertions) NotEqualf(expected interface{}, actual interface{}, msg str NotEqualf(a.t, expected, actual, msg, args...) } -// NotErrorIs asserts that at none of the errors in err's chain matches target. +// NotErrorAs asserts that none of the errors in err's chain matches target, +// but if so, sets target to that error value. +func (a *Assertions) NotErrorAs(err error, target interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotErrorAs(a.t, err, target, msgAndArgs...) +} + +// NotErrorAsf asserts that none of the errors in err's chain matches target, +// but if so, sets target to that error value. +func (a *Assertions) NotErrorAsf(err error, target interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotErrorAsf(a.t, err, target, msg, args...) +} + +// NotErrorIs asserts that none of the errors in err's chain matches target. // This is a wrapper for errors.Is. func (a *Assertions) NotErrorIs(err error, target error, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { @@ -1210,7 +1262,7 @@ func (a *Assertions) NotErrorIs(err error, target error, msgAndArgs ...interface NotErrorIs(a.t, err, target, msgAndArgs...) } -// NotErrorIsf asserts that at none of the errors in err's chain matches target. +// NotErrorIsf asserts that none of the errors in err's chain matches target. // This is a wrapper for errors.Is. func (a *Assertions) NotErrorIsf(err error, target error, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { diff --git a/vendor/github.com/stretchr/testify/require/requirements.go b/vendor/github.com/stretchr/testify/require/requirements.go index 91772dfeb..6b7ce929e 100644 --- a/vendor/github.com/stretchr/testify/require/requirements.go +++ b/vendor/github.com/stretchr/testify/require/requirements.go @@ -6,7 +6,7 @@ type TestingT interface { FailNow() } -type tHelper interface { +type tHelper = interface { Helper() } diff --git a/vendor/github.com/stretchr/testify/suite/doc.go b/vendor/github.com/stretchr/testify/suite/doc.go index 8d55a3aa8..05a562f72 100644 --- a/vendor/github.com/stretchr/testify/suite/doc.go +++ b/vendor/github.com/stretchr/testify/suite/doc.go @@ -5,6 +5,8 @@ // or individual tests (depending on which interface(s) you // implement). // +// The suite package does not support parallel tests. See [issue 934]. +// // A testing suite is usually built by first extending the built-in // suite functionality from suite.Suite in testify. Alternatively, // you could reproduce that logic on your own if you wanted (you @@ -63,4 +65,6 @@ // func TestExampleTestSuite(t *testing.T) { // suite.Run(t, new(ExampleTestSuite)) // } +// +// [issue 934]: https://github.com/stretchr/testify/issues/934 package suite diff --git a/vendor/modules.txt b/vendor/modules.txt index 2f8c861a2..8af740067 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -631,9 +631,10 @@ github.com/spf13/viper/internal/encoding/json github.com/spf13/viper/internal/encoding/toml github.com/spf13/viper/internal/encoding/yaml github.com/spf13/viper/internal/features -# github.com/stretchr/testify v1.9.0 +# github.com/stretchr/testify v1.10.0 ## explicit; go 1.17 github.com/stretchr/testify/assert +github.com/stretchr/testify/assert/yaml github.com/stretchr/testify/require github.com/stretchr/testify/suite # github.com/subosito/gotenv v1.6.0 From cac9d65029e972af9440ff79a2617d5c524a9d64 Mon Sep 17 00:00:00 2001 From: kim <89579420+NyaaaWhatsUpDoc@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:48:59 +0000 Subject: [PATCH 04/26] [performance] convert enum strings to ints (#3558) * convert statuses.visibility and notifications.notification_type columns from type string -> int for performance / space savings * fix test trying to compare string to int * fix instance count query using string literal instead of gtsmodel const type * ensure a default value is always set * also migrate the account settings and sin bin status tables * initialize maps outside loops and place into singular enum mapping creation func * use int16 for enum types * update sinbinstatus creation to be from a snapshot at initial creation * add snapshot of poll type at creation time --- internal/ap/extract.go | 2 +- .../api/client/accounts/accountverify_test.go | 3 +- .../client/notifications/notificationsget.go | 16 +- internal/api/util/parsequery.go | 47 ++++ internal/db/bundb/account_test.go | 2 +- internal/db/bundb/instance.go | 2 +- .../20231002153327_add_status_polls.go | 3 +- .../20231002153327_add_status_polls/polls.go | 48 ++++ .../20231002153327_add_status_polls/status.go | 75 ++++++ .../20240620074530_interaction_policy.go | 5 +- .../status.go | 93 ++++--- ...40904084406_fedi_api_reject_interaction.go | 2 +- .../sinbinstatus.go | 66 +++++ .../20241121121623_enum_strings_to_ints.go | 249 ++++++++++++++++++ .../accountsettings.go | 45 ++++ .../notification.go | 57 ++++ .../sinbinstatus.go | 45 ++++ .../status.go | 95 +++++++ internal/db/bundb/notification.go | 6 +- internal/db/bundb/relationship_follow_req.go | 8 +- internal/db/notification.go | 4 +- internal/gtsmodel/accountsettings.go | 4 +- internal/gtsmodel/common.go | 24 ++ internal/gtsmodel/interactionpolicy.go | 2 +- internal/gtsmodel/notification.go | 59 ++++- internal/gtsmodel/status.go | 55 +++- internal/processing/preferences.go | 2 +- internal/processing/status/create.go | 2 +- internal/processing/timeline/notification.go | 4 +- internal/processing/workers/surfacenotify.go | 2 +- internal/typeutils/frontendtointernal.go | 2 +- internal/typeutils/internaltofrontend.go | 2 +- 32 files changed, 940 insertions(+), 91 deletions(-) create mode 100644 internal/db/bundb/migrations/20231002153327_add_status_polls/polls.go create mode 100644 internal/db/bundb/migrations/20231002153327_add_status_polls/status.go create mode 100644 internal/db/bundb/migrations/20240904084406_fedi_api_reject_interaction/sinbinstatus.go create mode 100644 internal/db/bundb/migrations/20241121121623_enum_strings_to_ints.go create mode 100644 internal/db/bundb/migrations/20241121121623_enum_strings_to_ints/accountsettings.go create mode 100644 internal/db/bundb/migrations/20241121121623_enum_strings_to_ints/notification.go create mode 100644 internal/db/bundb/migrations/20241121121623_enum_strings_to_ints/sinbinstatus.go create mode 100644 internal/db/bundb/migrations/20241121121623_enum_strings_to_ints/status.go create mode 100644 internal/gtsmodel/common.go diff --git a/internal/ap/extract.go b/internal/ap/extract.go index f5486a051..543ee8dca 100644 --- a/internal/ap/extract.go +++ b/internal/ap/extract.go @@ -1027,7 +1027,7 @@ func ExtractVisibility(addressable Addressable, actorFollowersURI string) (gtsmo ) if len(to) == 0 && len(cc) == 0 { - return "", gtserror.Newf("message wasn't TO or CC anyone") + return 0, gtserror.Newf("message wasn't TO or CC anyone") } // Assume most restrictive visibility, diff --git a/internal/api/client/accounts/accountverify_test.go b/internal/api/client/accounts/accountverify_test.go index af8c2346c..3f67cdefb 100644 --- a/internal/api/client/accounts/accountverify_test.go +++ b/internal/api/client/accounts/accountverify_test.go @@ -28,7 +28,6 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/api/client/accounts" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -99,7 +98,7 @@ func (suite *AccountVerifyTestSuite) TestAccountVerifyGet() { suite.Equal(2, apimodelAccount.FollowersCount) suite.Equal(2, apimodelAccount.FollowingCount) suite.Equal(8, apimodelAccount.StatusesCount) - suite.EqualValues(gtsmodel.VisibilityPublic, apimodelAccount.Source.Privacy) + suite.EqualValues(apimodel.VisibilityPublic, apimodelAccount.Source.Privacy) suite.Equal(testAccount.Settings.Language, apimodelAccount.Source.Language) suite.Equal(testAccount.NoteRaw, apimodelAccount.Source.Note) } diff --git a/internal/api/client/notifications/notificationsget.go b/internal/api/client/notifications/notificationsget.go index f7bcf1994..cc3e5bdb7 100644 --- a/internal/api/client/notifications/notificationsget.go +++ b/internal/api/client/notifications/notificationsget.go @@ -164,6 +164,18 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) { limit = int(i) } + types, errWithCode := apiutil.ParseNotificationTypes(c.QueryArray(TypesKey)) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + exclTypes, errWithCode := apiutil.ParseNotificationTypes(c.QueryArray(ExcludeTypesKey)) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + resp, errWithCode := m.processor.Timeline().NotificationsGet( c.Request.Context(), authed, @@ -171,8 +183,8 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) { c.Query(SinceIDKey), c.Query(MinIDKey), limit, - c.QueryArray(TypesKey), - c.QueryArray(ExcludeTypesKey), + types, + exclTypes, ) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) diff --git a/internal/api/util/parsequery.go b/internal/api/util/parsequery.go index 9f4c02aed..9929524c5 100644 --- a/internal/api/util/parsequery.go +++ b/internal/api/util/parsequery.go @@ -18,11 +18,13 @@ package util import ( + "errors" "fmt" "strconv" "strings" "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) const ( @@ -216,6 +218,51 @@ func ParseInteractionReblogs(value string, defaultValue bool) (bool, gtserror.Wi return parseBool(value, defaultValue, InteractionReblogsKey) } +func ParseNotificationType(value string) (gtsmodel.NotificationType, gtserror.WithCode) { + switch strings.ToLower(value) { + case "follow": + return gtsmodel.NotificationFollow, nil + case "follow_request": + return gtsmodel.NotificationFollowRequest, nil + case "mention": + return gtsmodel.NotificationMention, nil + case "reblog": + return gtsmodel.NotificationReblog, nil + case "favourite": + return gtsmodel.NotificationFave, nil + case "poll": + return gtsmodel.NotificationPoll, nil + case "status": + return gtsmodel.NotificationStatus, nil + case "admin.sign_up": + return gtsmodel.NotificationSignup, nil + case "pending.favourite": + return gtsmodel.NotificationPendingFave, nil + case "pending.reply": + return gtsmodel.NotificationPendingReply, nil + case "pending.reblog": + return gtsmodel.NotificationPendingReblog, nil + default: + text := fmt.Sprintf("unrecognized notification type %s", value) + return 0, gtserror.NewErrorBadRequest(errors.New(text), text) + } +} + +func ParseNotificationTypes(values []string) ([]gtsmodel.NotificationType, gtserror.WithCode) { + if len(values) == 0 { + return nil, nil + } + ntypes := make([]gtsmodel.NotificationType, len(values)) + for i, value := range values { + ntype, errWithCode := ParseNotificationType(value) + if errWithCode != nil { + return nil, errWithCode + } + ntypes[i] = ntype + } + return ntypes, nil +} + /* Parse functions for *REQUIRED* parameters. */ diff --git a/internal/db/bundb/account_test.go b/internal/db/bundb/account_test.go index 2caffdeb1..7dcc0f9e7 100644 --- a/internal/db/bundb/account_test.go +++ b/internal/db/bundb/account_test.go @@ -106,7 +106,7 @@ func (suite *AccountTestSuite) populateTestStatus(testAccountKey string, status status.URI = fmt.Sprintf("http://localhost:8080/users/%s/statuses/%s", testAccount.Username, status.ID) status.Local = util.Ptr(true) - if status.Visibility == "" { + if status.Visibility == 0 { status.Visibility = gtsmodel.VisibilityDefault } if status.ActivityStreamsType == "" { diff --git a/internal/db/bundb/instance.go b/internal/db/bundb/instance.go index bbfd82ffb..613a2b13a 100644 --- a/internal/db/bundb/instance.go +++ b/internal/db/bundb/instance.go @@ -104,7 +104,7 @@ func (i *instanceDB) CountInstanceStatuses(ctx context.Context, domain string) ( q = q.Where("NOT ? = ?", bun.Ident("status.pending_approval"), true) // Ignore statuses that are direct messages. - q = q.Where("NOT ? = ?", bun.Ident("status.visibility"), "direct") + q = q.Where("NOT ? = ?", bun.Ident("status.visibility"), gtsmodel.VisibilityDirect) count, err := q.Count(ctx) if err != nil { diff --git a/internal/db/bundb/migrations/20231002153327_add_status_polls.go b/internal/db/bundb/migrations/20231002153327_add_status_polls.go index 5e525cc27..019a369d4 100644 --- a/internal/db/bundb/migrations/20231002153327_add_status_polls.go +++ b/internal/db/bundb/migrations/20231002153327_add_status_polls.go @@ -21,7 +21,8 @@ import ( "context" "strings" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20231002153327_add_status_polls" + "github.com/uptrace/bun" ) diff --git a/internal/db/bundb/migrations/20231002153327_add_status_polls/polls.go b/internal/db/bundb/migrations/20231002153327_add_status_polls/polls.go new file mode 100644 index 000000000..c3e03d267 --- /dev/null +++ b/internal/db/bundb/migrations/20231002153327_add_status_polls/polls.go @@ -0,0 +1,48 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package gtsmodel + +import ( + "time" +) + +// Poll represents an attached (to) Status poll, i.e. a questionaire. Can be remote / local. +type Poll struct { + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // Unique identity string. + Multiple *bool `bun:",nullzero,notnull,default:false"` // Is this a multiple choice poll? i.e. can you vote on multiple options. + HideCounts *bool `bun:",nullzero,notnull,default:false"` // Hides vote counts until poll ends. + Options []string `bun:",nullzero,notnull"` // The available options for this poll. + Votes []int `bun:",nullzero,notnull"` // Vote counts per choice. + Voters *int `bun:",nullzero,notnull"` // Total no. voters count. + StatusID string `bun:"type:CHAR(26),nullzero,notnull,unique"` // Status ID of which this Poll is attached to. + ExpiresAt time.Time `bun:"type:timestamptz,nullzero,notnull"` // The expiry date of this Poll. + ClosedAt time.Time `bun:"type:timestamptz,nullzero"` // The closure date of this poll, will be zerotime until set. + Closing bool `bun:"-"` // An ephemeral field only set on Polls in the middle of closing. + // no creation date, use attached Status.CreatedAt. +} + +// PollVote represents a single instance of vote(s) in a Poll by an account. +// If the Poll is single-choice, len(.Choices) = 1, if multiple-choice then +// len(.Choices) >= 1. Can be remote or local. +type PollVote struct { + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // Unique identity string. + Choices []int `bun:",nullzero,notnull"` // The Poll's option indices of which these are votes for. + AccountID string `bun:"type:CHAR(26),nullzero,notnull,unique:in_poll_by_account"` // Account ID from which this vote originated. + PollID string `bun:"type:CHAR(26),nullzero,notnull,unique:in_poll_by_account"` // Poll ID of which this is a vote in. + CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // The creation date of this PollVote. +} diff --git a/internal/db/bundb/migrations/20231002153327_add_status_polls/status.go b/internal/db/bundb/migrations/20231002153327_add_status_polls/status.go new file mode 100644 index 000000000..8e3252e82 --- /dev/null +++ b/internal/db/bundb/migrations/20231002153327_add_status_polls/status.go @@ -0,0 +1,75 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package gtsmodel + +import "time" + +// Status represents a user-created 'post' or 'status' in the database, either remote or local +type Status struct { + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database + CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created + UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated + FetchedAt time.Time `bun:"type:timestamptz,nullzero"` // when was item (remote) last fetched. + PinnedAt time.Time `bun:"type:timestamptz,nullzero"` // Status was pinned by owning account at this time. + URI string `bun:",unique,nullzero,notnull"` // activitypub URI of this status + URL string `bun:",nullzero"` // web url for viewing this status + Content string `bun:""` // content of this status; likely html-formatted but not guaranteed + AttachmentIDs []string `bun:"attachments,array"` // Database IDs of any media attachments associated with this status + TagIDs []string `bun:"tags,array"` // Database IDs of any tags used in this status + MentionIDs []string `bun:"mentions,array"` // Database IDs of any mentions in this status + EmojiIDs []string `bun:"emojis,array"` // Database IDs of any emojis used in this status + Local *bool `bun:",nullzero,notnull,default:false"` // is this status from a local account? + AccountID string `bun:"type:CHAR(26),nullzero,notnull"` // which account posted this status? + AccountURI string `bun:",nullzero,notnull"` // activitypub uri of the owner of this status + InReplyToID string `bun:"type:CHAR(26),nullzero"` // id of the status this status replies to + InReplyToURI string `bun:",nullzero"` // activitypub uri of the status this status is a reply to + InReplyToAccountID string `bun:"type:CHAR(26),nullzero"` // id of the account that this status replies to + BoostOfID string `bun:"type:CHAR(26),nullzero"` // id of the status this status is a boost of + BoostOfAccountID string `bun:"type:CHAR(26),nullzero"` // id of the account that owns the boosted status + ThreadID string `bun:"type:CHAR(26),nullzero"` // id of the thread to which this status belongs; only set for remote statuses if a local account is involved at some point in the thread, otherwise null + PollID string `bun:"type:CHAR(26),nullzero"` // + ContentWarning string `bun:",nullzero"` // cw string for this status + Visibility Visibility `bun:",nullzero,notnull"` // visibility entry for this status + Sensitive *bool `bun:",nullzero,notnull,default:false"` // mark the status as sensitive? + Language string `bun:",nullzero"` // what language is this status written in? + CreatedWithApplicationID string `bun:"type:CHAR(26),nullzero"` // Which application was used to create this status? + ActivityStreamsType string `bun:",nullzero,notnull"` // What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types. Will probably almost always be Note but who knows!. + Text string `bun:""` // Original text of the status without formatting + Federated *bool `bun:",notnull"` // This status will be federated beyond the local timeline(s) + Boostable *bool `bun:",notnull"` // This status can be boosted/reblogged + Replyable *bool `bun:",notnull"` // This status can be replied to + Likeable *bool `bun:",notnull"` // This status can be liked/faved +} + +// Visibility represents the visibility granularity of a status. +type Visibility string + +const ( + // VisibilityPublic means this status will be visible to everyone on all timelines. + VisibilityPublic Visibility = "public" + // VisibilityUnlocked means this status will be visible to everyone, but will only show on home timeline to followers, and in lists. + VisibilityUnlocked Visibility = "unlocked" + // VisibilityFollowersOnly means this status is viewable to followers only. + VisibilityFollowersOnly Visibility = "followers_only" + // VisibilityMutualsOnly means this status is visible to mutual followers only. + VisibilityMutualsOnly Visibility = "mutuals_only" + // VisibilityDirect means this status is visible only to mentioned recipients. + VisibilityDirect Visibility = "direct" + // VisibilityDefault is used when no other setting can be found. + VisibilityDefault Visibility = VisibilityUnlocked +) diff --git a/internal/db/bundb/migrations/20240620074530_interaction_policy.go b/internal/db/bundb/migrations/20240620074530_interaction_policy.go index bbc75d9ec..7678af7ed 100644 --- a/internal/db/bundb/migrations/20240620074530_interaction_policy.go +++ b/internal/db/bundb/migrations/20240620074530_interaction_policy.go @@ -161,11 +161,14 @@ func init() { return err } + // Get the mapping of old enum string values to new integer values. + visibilityMapping := visibilityEnumMapping[oldmodel.Visibility]() + // For each status found in this way, update // to new version of interaction policy. for _, oldStatus := range oldStatuses { // Start with default policy for this visibility. - v := gtsmodel.Visibility(oldStatus.Visibility) + v := visibilityMapping[oldStatus.Visibility] policy := gtsmodel.DefaultInteractionPolicyFor(v) if !*oldStatus.Likeable { diff --git a/internal/db/bundb/migrations/20240620074530_interaction_policy/status.go b/internal/db/bundb/migrations/20240620074530_interaction_policy/status.go index ae96d047d..615c81b66 100644 --- a/internal/db/bundb/migrations/20240620074530_interaction_policy/status.go +++ b/internal/db/bundb/migrations/20240620074530_interaction_policy/status.go @@ -22,40 +22,61 @@ import ( ) type Status struct { - ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` - CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` - UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` - FetchedAt time.Time `bun:"type:timestamptz,nullzero"` - PinnedAt time.Time `bun:"type:timestamptz,nullzero"` - URI string `bun:",unique,nullzero,notnull"` - URL string `bun:",nullzero"` - Content string `bun:""` - AttachmentIDs []string `bun:"attachments,array"` - TagIDs []string `bun:"tags,array"` - MentionIDs []string `bun:"mentions,array"` - EmojiIDs []string `bun:"emojis,array"` - Local *bool `bun:",nullzero,notnull,default:false"` - AccountID string `bun:"type:CHAR(26),nullzero,notnull"` - AccountURI string `bun:",nullzero,notnull"` - InReplyToID string `bun:"type:CHAR(26),nullzero"` - InReplyToURI string `bun:",nullzero"` - InReplyToAccountID string `bun:"type:CHAR(26),nullzero"` - InReplyTo *Status `bun:"-"` - BoostOfID string `bun:"type:CHAR(26),nullzero"` - BoostOfURI string `bun:"-"` - BoostOfAccountID string `bun:"type:CHAR(26),nullzero"` - BoostOf *Status `bun:"-"` - ThreadID string `bun:"type:CHAR(26),nullzero"` - PollID string `bun:"type:CHAR(26),nullzero"` - ContentWarning string `bun:",nullzero"` - Visibility string `bun:",nullzero,notnull"` - Sensitive *bool `bun:",nullzero,notnull,default:false"` - Language string `bun:",nullzero"` - CreatedWithApplicationID string `bun:"type:CHAR(26),nullzero"` - ActivityStreamsType string `bun:",nullzero,notnull"` - Text string `bun:""` - Federated *bool `bun:",notnull"` - Boostable *bool `bun:",notnull"` - Replyable *bool `bun:",notnull"` - Likeable *bool `bun:",notnull"` + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` + CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` + UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` + FetchedAt time.Time `bun:"type:timestamptz,nullzero"` + PinnedAt time.Time `bun:"type:timestamptz,nullzero"` + URI string `bun:",unique,nullzero,notnull"` + URL string `bun:",nullzero"` + Content string `bun:""` + AttachmentIDs []string `bun:"attachments,array"` + TagIDs []string `bun:"tags,array"` + MentionIDs []string `bun:"mentions,array"` + EmojiIDs []string `bun:"emojis,array"` + Local *bool `bun:",nullzero,notnull,default:false"` + AccountID string `bun:"type:CHAR(26),nullzero,notnull"` + AccountURI string `bun:",nullzero,notnull"` + InReplyToID string `bun:"type:CHAR(26),nullzero"` + InReplyToURI string `bun:",nullzero"` + InReplyToAccountID string `bun:"type:CHAR(26),nullzero"` + InReplyTo *Status `bun:"-"` + BoostOfID string `bun:"type:CHAR(26),nullzero"` + BoostOfURI string `bun:"-"` + BoostOfAccountID string `bun:"type:CHAR(26),nullzero"` + BoostOf *Status `bun:"-"` + ThreadID string `bun:"type:CHAR(26),nullzero"` + PollID string `bun:"type:CHAR(26),nullzero"` + ContentWarning string `bun:",nullzero"` + Visibility Visibility `bun:",nullzero,notnull"` + Sensitive *bool `bun:",nullzero,notnull,default:false"` + Language string `bun:",nullzero"` + CreatedWithApplicationID string `bun:"type:CHAR(26),nullzero"` + ActivityStreamsType string `bun:",nullzero,notnull"` + Text string `bun:""` + Federated *bool `bun:",notnull"` + Boostable *bool `bun:",notnull"` + Replyable *bool `bun:",notnull"` + Likeable *bool `bun:",notnull"` } + +// Visibility represents the visibility granularity of a status. +type Visibility string + +const ( + // VisibilityNone means nobody can see this. + // It's only used for web status visibility. + VisibilityNone Visibility = "none" + // VisibilityPublic means this status will be visible to everyone on all timelines. + VisibilityPublic Visibility = "public" + // VisibilityUnlocked means this status will be visible to everyone, but will only show on home timeline to followers, and in lists. + VisibilityUnlocked Visibility = "unlocked" + // VisibilityFollowersOnly means this status is viewable to followers only. + VisibilityFollowersOnly Visibility = "followers_only" + // VisibilityMutualsOnly means this status is visible to mutual followers only. + VisibilityMutualsOnly Visibility = "mutuals_only" + // VisibilityDirect means this status is visible only to mentioned recipients. + VisibilityDirect Visibility = "direct" + // VisibilityDefault is used when no other setting can be found. + VisibilityDefault Visibility = VisibilityUnlocked +) diff --git a/internal/db/bundb/migrations/20240904084406_fedi_api_reject_interaction.go b/internal/db/bundb/migrations/20240904084406_fedi_api_reject_interaction.go index d97d35372..d3c0f49a4 100644 --- a/internal/db/bundb/migrations/20240904084406_fedi_api_reject_interaction.go +++ b/internal/db/bundb/migrations/20240904084406_fedi_api_reject_interaction.go @@ -20,7 +20,7 @@ package migrations import ( "context" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20240904084406_fedi_api_reject_interaction" "github.com/uptrace/bun" ) diff --git a/internal/db/bundb/migrations/20240904084406_fedi_api_reject_interaction/sinbinstatus.go b/internal/db/bundb/migrations/20240904084406_fedi_api_reject_interaction/sinbinstatus.go new file mode 100644 index 000000000..e18affa9b --- /dev/null +++ b/internal/db/bundb/migrations/20240904084406_fedi_api_reject_interaction/sinbinstatus.go @@ -0,0 +1,66 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package gtsmodel + +import "time" + +// SinBinStatus represents a status that's been rejected and/or reported + quarantined. +// +// Automatically rejected statuses are not put in the sin bin, only statuses that were +// stored on the instance and which someone (local or remote) has subsequently rejected. +type SinBinStatus struct { + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // ID of this item in the database. + CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // Creation time of this item. + UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // Last-updated time of this item. + URI string `bun:",unique,nullzero,notnull"` // ActivityPub URI/ID of this status. + URL string `bun:",nullzero"` // Web url for viewing this status. + Domain string `bun:",nullzero"` // Domain of the status, will be null if this is a local status, otherwise something like `example.org`. + AccountURI string `bun:",nullzero,notnull"` // ActivityPub uri of the author of this status. + InReplyToURI string `bun:",nullzero"` // ActivityPub uri of the status this status is a reply to. + Content string `bun:",nullzero"` // Content of this status. + AttachmentLinks []string `bun:",nullzero,array"` // Links to attachments of this status. + MentionTargetURIs []string `bun:",nullzero,array"` // URIs of mentioned accounts. + EmojiLinks []string `bun:",nullzero,array"` // Links to any emoji images used in this status. + PollOptions []string `bun:",nullzero,array"` // String values of any poll options used in this status. + ContentWarning string `bun:",nullzero"` // CW / subject string for this status. + Visibility Visibility `bun:",nullzero,notnull"` // Visibility level of this status. + Sensitive *bool `bun:",nullzero,notnull,default:false"` // Mark the status as sensitive. + Language string `bun:",nullzero"` // Language code for this status. + ActivityStreamsType string `bun:",nullzero,notnull"` // ActivityStreams type of this status. +} + +// Visibility represents the visibility granularity of a status. +type Visibility string + +const ( + // VisibilityNone means nobody can see this. + // It's only used for web status visibility. + VisibilityNone Visibility = "none" + // VisibilityPublic means this status will be visible to everyone on all timelines. + VisibilityPublic Visibility = "public" + // VisibilityUnlocked means this status will be visible to everyone, but will only show on home timeline to followers, and in lists. + VisibilityUnlocked Visibility = "unlocked" + // VisibilityFollowersOnly means this status is viewable to followers only. + VisibilityFollowersOnly Visibility = "followers_only" + // VisibilityMutualsOnly means this status is visible to mutual followers only. + VisibilityMutualsOnly Visibility = "mutuals_only" + // VisibilityDirect means this status is visible only to mentioned recipients. + VisibilityDirect Visibility = "direct" + // VisibilityDefault is used when no other setting can be found. + VisibilityDefault Visibility = VisibilityUnlocked +) diff --git a/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints.go b/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints.go new file mode 100644 index 000000000..10ae95c17 --- /dev/null +++ b/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints.go @@ -0,0 +1,249 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package migrations + +import ( + "context" + "errors" + + old_gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + new_gtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" + "github.com/superseriousbusiness/gotosocial/internal/util" + + "github.com/uptrace/bun" +) + +func init() { + up := func(ctx context.Context, db *bun.DB) error { + return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + + // Tables with visibility types. + var visTables = []struct { + Table string + Column string + Default *new_gtsmodel.Visibility + }{ + {Table: "statuses", Column: "visibility"}, + {Table: "sin_bin_statuses", Column: "visibility"}, + {Table: "account_settings", Column: "privacy", Default: util.Ptr(new_gtsmodel.VisibilityDefault)}, + {Table: "account_settings", Column: "web_visibility", Default: util.Ptr(new_gtsmodel.VisibilityDefault)}, + } + + // Visibility type indices. + var visIndices = []struct { + name string + cols []string + order string + }{ + { + name: "statuses_visibility_idx", + cols: []string{"visibility"}, + order: "", + }, + { + name: "statuses_profile_web_view_idx", + cols: []string{"account_id", "visibility"}, + order: "id DESC", + }, + { + name: "statuses_public_timeline_idx", + cols: []string{"visibility"}, + order: "id DESC", + }, + } + + // Before making changes to the visibility col + // we must drop all indices that rely on it. + for _, index := range visIndices { + if _, err := tx.NewDropIndex(). + Index(index.name). + Exec(ctx); err != nil { + return err + } + } + + // Get the mapping of old enum string values to new integer values. + visibilityMapping := visibilityEnumMapping[old_gtsmodel.Visibility]() + + // Convert all visibility tables. + for _, table := range visTables { + if err := convertEnums(ctx, tx, table.Table, table.Column, + visibilityMapping, table.Default); err != nil { + return err + } + } + + // Recreate the visibility indices. + for _, index := range visIndices { + q := tx.NewCreateIndex(). + Table("statuses"). + Index(index.name). + Column(index.cols...) + if index.order != "" { + q = q.ColumnExpr(index.order) + } + if _, err := q.Exec(ctx); err != nil { + return err + } + } + + // Get the mapping of old enum string values to the new integer value types. + notificationMapping := notificationEnumMapping[old_gtsmodel.NotificationType]() + + // Migrate over old notifications table column over to new column type. + if err := convertEnums(ctx, tx, "notifications", "notification_type", //nolint:revive + notificationMapping, nil); err != nil { + return err + } + + return nil + }) + } + + down := func(ctx context.Context, db *bun.DB) error { + return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + return nil + }) + } + + if err := Migrations.Register(up, down); err != nil { + panic(err) + } +} + +// convertEnums performs a transaction that converts +// a table's column of our old-style enums (strings) to +// more performant and space-saving integer types. +func convertEnums[OldType ~string, NewType ~int16]( + ctx context.Context, + tx bun.Tx, + table string, + column string, + mapping map[OldType]NewType, + defaultValue *NewType, +) error { + if len(mapping) == 0 { + return errors.New("empty mapping") + } + + // Generate new column name. + newColumn := column + "_new" + + log.Infof(ctx, "converting %s.%s enums; "+ + "this may take a while, please don't interrupt!", + table, column, + ) + + // Ensure a default value. + if defaultValue == nil { + var zero NewType + defaultValue = &zero + } + + // Add new column to database. + if _, err := tx.NewAddColumn(). + Table(table). + ColumnExpr("? SMALLINT NOT NULL DEFAULT ?", + bun.Ident(newColumn), + *defaultValue). + Exec(ctx); err != nil { + return gtserror.Newf("error adding new column: %w", err) + } + + // Get a count of all in table. + total, err := tx.NewSelect(). + Table(table). + Count(ctx) + if err != nil { + return gtserror.Newf("error selecting total count: %w", err) + } + + var updated int + for old, new := range mapping { + + // Update old to new values. + res, err := tx.NewUpdate(). + Table(table). + Where("? = ?", bun.Ident(column), old). + Set("? = ?", bun.Ident(newColumn), new). + Exec(ctx) + if err != nil { + return gtserror.Newf("error updating old column values: %w", err) + } + + // Count number items updated. + n, _ := res.RowsAffected() + updated += int(n) + } + + // Check total updated. + if total != updated { + log.Warnf(ctx, "total=%d does not match updated=%d", total, updated) + } + + // Drop the old column from table. + if _, err := tx.NewDropColumn(). + Table(table). + ColumnExpr("?", bun.Ident(column)). + Exec(ctx); err != nil { + return gtserror.Newf("error dropping old column: %w", err) + } + + // Rename new to old name. + if _, err := tx.NewRaw( + "ALTER TABLE ? RENAME COLUMN ? TO ?", + bun.Ident(table), + bun.Ident(newColumn), + bun.Ident(column), + ).Exec(ctx); err != nil { + return gtserror.Newf("error renaming new column: %w", err) + } + + return nil +} + +// visibilityEnumMapping maps old Visibility enum values to their newer integer type. +func visibilityEnumMapping[T ~string]() map[T]new_gtsmodel.Visibility { + return map[T]new_gtsmodel.Visibility{ + T(old_gtsmodel.VisibilityNone): new_gtsmodel.VisibilityNone, + T(old_gtsmodel.VisibilityPublic): new_gtsmodel.VisibilityPublic, + T(old_gtsmodel.VisibilityUnlocked): new_gtsmodel.VisibilityUnlocked, + T(old_gtsmodel.VisibilityFollowersOnly): new_gtsmodel.VisibilityFollowersOnly, + T(old_gtsmodel.VisibilityMutualsOnly): new_gtsmodel.VisibilityMutualsOnly, + T(old_gtsmodel.VisibilityDirect): new_gtsmodel.VisibilityDirect, + } +} + +// notificationEnumMapping maps old NotificationType enum values to their newer integer type. +func notificationEnumMapping[T ~string]() map[T]new_gtsmodel.NotificationType { + return map[T]new_gtsmodel.NotificationType{ + T(old_gtsmodel.NotificationFollow): new_gtsmodel.NotificationFollow, + T(old_gtsmodel.NotificationFollowRequest): new_gtsmodel.NotificationFollowRequest, + T(old_gtsmodel.NotificationMention): new_gtsmodel.NotificationMention, + T(old_gtsmodel.NotificationReblog): new_gtsmodel.NotificationReblog, + T(old_gtsmodel.NotificationFave): new_gtsmodel.NotificationFave, + T(old_gtsmodel.NotificationPoll): new_gtsmodel.NotificationPoll, + T(old_gtsmodel.NotificationStatus): new_gtsmodel.NotificationStatus, + T(old_gtsmodel.NotificationSignup): new_gtsmodel.NotificationSignup, + T(old_gtsmodel.NotificationPendingFave): new_gtsmodel.NotificationPendingFave, + T(old_gtsmodel.NotificationPendingReply): new_gtsmodel.NotificationPendingReply, + T(old_gtsmodel.NotificationPendingReblog): new_gtsmodel.NotificationPendingReblog, + } +} diff --git a/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints/accountsettings.go b/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints/accountsettings.go new file mode 100644 index 000000000..9a9cfd8e1 --- /dev/null +++ b/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints/accountsettings.go @@ -0,0 +1,45 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package gtsmodel + +import ( + "time" + + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +// AccountSettings models settings / preferences for a local, non-instance account. +type AccountSettings struct { + AccountID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // AccountID that owns this settings. + CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created. + UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item was last updated. + Privacy Visibility `bun:",nullzero,default:3"` // Default post privacy for this account + Sensitive *bool `bun:",nullzero,notnull,default:false"` // Set posts from this account to sensitive by default? + Language string `bun:",nullzero,notnull,default:'en'"` // What language does this account post in? + StatusContentType string `bun:",nullzero"` // What is the default format for statuses posted by this account (only for local accounts). + Theme string `bun:",nullzero"` // Preset CSS theme filename selected by this Account (empty string if nothing set). + CustomCSS string `bun:",nullzero"` // Custom CSS that should be displayed for this Account's profile and statuses. + EnableRSS *bool `bun:",nullzero,notnull,default:false"` // enable RSS feed subscription for this account's public posts at [URL]/feed + HideCollections *bool `bun:",nullzero,notnull,default:false"` // Hide this account's followers/following collections. + WebVisibility Visibility `bun:",nullzero,notnull,default:3"` // Visibility level of statuses that visitors can view via the web profile. + InteractionPolicyDirect *gtsmodel.InteractionPolicy `bun:""` // Interaction policy to use for new direct visibility statuses by this account. If null, assume default policy. + InteractionPolicyMutualsOnly *gtsmodel.InteractionPolicy `bun:""` // Interaction policy to use for new mutuals only visibility statuses. If null, assume default policy. + InteractionPolicyFollowersOnly *gtsmodel.InteractionPolicy `bun:""` // Interaction policy to use for new followers only visibility statuses. If null, assume default policy. + InteractionPolicyUnlocked *gtsmodel.InteractionPolicy `bun:""` // Interaction policy to use for new unlocked visibility statuses. If null, assume default policy. + InteractionPolicyPublic *gtsmodel.InteractionPolicy `bun:""` // Interaction policy to use for new public visibility statuses. If null, assume default policy. +} diff --git a/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints/notification.go b/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints/notification.go new file mode 100644 index 000000000..77166a35d --- /dev/null +++ b/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints/notification.go @@ -0,0 +1,57 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package gtsmodel + +import ( + "time" + + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +// Notification models an alert/notification sent to an account about something like a reblog, like, new follow request, etc. +type Notification struct { + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database + CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created + UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated + NotificationType NotificationType `bun:",nullzero,notnull"` // Type of this notification + TargetAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // ID of the account targeted by the notification (ie., who will receive the notification?) + TargetAccount *gtsmodel.Account `bun:"-"` // Account corresponding to TargetAccountID. Can be nil, always check first + select using ID if necessary. + OriginAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // ID of the account that performed the action that created the notification. + OriginAccount *gtsmodel.Account `bun:"-"` // Account corresponding to OriginAccountID. Can be nil, always check first + select using ID if necessary. + StatusID string `bun:"type:CHAR(26),nullzero"` // If the notification pertains to a status, what is the database ID of that status? + Status *Status `bun:"-"` // Status corresponding to StatusID. Can be nil, always check first + select using ID if necessary. + Read *bool `bun:",nullzero,notnull,default:false"` // Notification has been seen/read +} + +// NotificationType describes the reason/type of this notification. +type NotificationType string + +// Notification Types +const ( + NotificationFollow NotificationType = "follow" // NotificationFollow -- someone followed you + NotificationFollowRequest NotificationType = "follow_request" // NotificationFollowRequest -- someone requested to follow you + NotificationMention NotificationType = "mention" // NotificationMention -- someone mentioned you in their status + NotificationReblog NotificationType = "reblog" // NotificationReblog -- someone boosted one of your statuses + NotificationFave NotificationType = "favourite" // NotificationFave -- someone faved/liked one of your statuses + NotificationPoll NotificationType = "poll" // NotificationPoll -- a poll you voted in or created has ended + NotificationStatus NotificationType = "status" // NotificationStatus -- someone you enabled notifications for has posted a status. + NotificationSignup NotificationType = "admin.sign_up" // NotificationSignup -- someone has submitted a new account sign-up to the instance. + NotificationPendingFave NotificationType = "pending.favourite" // Someone has faved a status of yours, which requires approval by you. + NotificationPendingReply NotificationType = "pending.reply" // Someone has replied to a status of yours, which requires approval by you. + NotificationPendingReblog NotificationType = "pending.reblog" // Someone has boosted a status of yours, which requires approval by you. +) diff --git a/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints/sinbinstatus.go b/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints/sinbinstatus.go new file mode 100644 index 000000000..d1dfcddd1 --- /dev/null +++ b/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints/sinbinstatus.go @@ -0,0 +1,45 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package gtsmodel + +import "time" + +// SinBinStatus represents a status that's been rejected and/or reported + quarantined. +// +// Automatically rejected statuses are not put in the sin bin, only statuses that were +// stored on the instance and which someone (local or remote) has subsequently rejected. +type SinBinStatus struct { + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // ID of this item in the database. + CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // Creation time of this item. + UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // Last-updated time of this item. + URI string `bun:",unique,nullzero,notnull"` // ActivityPub URI/ID of this status. + URL string `bun:",nullzero"` // Web url for viewing this status. + Domain string `bun:",nullzero"` // Domain of the status, will be null if this is a local status, otherwise something like `example.org`. + AccountURI string `bun:",nullzero,notnull"` // ActivityPub uri of the author of this status. + InReplyToURI string `bun:",nullzero"` // ActivityPub uri of the status this status is a reply to. + Content string `bun:",nullzero"` // Content of this status. + AttachmentLinks []string `bun:",nullzero,array"` // Links to attachments of this status. + MentionTargetURIs []string `bun:",nullzero,array"` // URIs of mentioned accounts. + EmojiLinks []string `bun:",nullzero,array"` // Links to any emoji images used in this status. + PollOptions []string `bun:",nullzero,array"` // String values of any poll options used in this status. + ContentWarning string `bun:",nullzero"` // CW / subject string for this status. + Visibility Visibility `bun:",nullzero,notnull"` // Visibility level of this status. + Sensitive *bool `bun:",nullzero,notnull,default:false"` // Mark the status as sensitive. + Language string `bun:",nullzero"` // Language code for this status. + ActivityStreamsType string `bun:",nullzero,notnull"` // ActivityStreams type of this status. +} diff --git a/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints/status.go b/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints/status.go new file mode 100644 index 000000000..38583c7fc --- /dev/null +++ b/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints/status.go @@ -0,0 +1,95 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package gtsmodel + +import ( + "time" + + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +// Status represents a user-created 'post' or 'status' in the database, either remote or local +type Status struct { + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database + CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created + UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated + FetchedAt time.Time `bun:"type:timestamptz,nullzero"` // when was item (remote) last fetched. + PinnedAt time.Time `bun:"type:timestamptz,nullzero"` // Status was pinned by owning account at this time. + URI string `bun:",unique,nullzero,notnull"` // activitypub URI of this status + URL string `bun:",nullzero"` // web url for viewing this status + Content string `bun:""` // content of this status; likely html-formatted but not guaranteed + AttachmentIDs []string `bun:"attachments,array"` // Database IDs of any media attachments associated with this status + Attachments []*gtsmodel.MediaAttachment `bun:"attached_media,rel:has-many"` // Attachments corresponding to attachmentIDs + TagIDs []string `bun:"tags,array"` // Database IDs of any tags used in this status + Tags []*gtsmodel.Tag `bun:"attached_tags,m2m:status_to_tags"` // Tags corresponding to tagIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation + MentionIDs []string `bun:"mentions,array"` // Database IDs of any mentions in this status + Mentions []*gtsmodel.Mention `bun:"attached_mentions,rel:has-many"` // Mentions corresponding to mentionIDs + EmojiIDs []string `bun:"emojis,array"` // Database IDs of any emojis used in this status + Emojis []*gtsmodel.Emoji `bun:"attached_emojis,m2m:status_to_emojis"` // Emojis corresponding to emojiIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation + Local *bool `bun:",nullzero,notnull,default:false"` // is this status from a local account? + AccountID string `bun:"type:CHAR(26),nullzero,notnull"` // which account posted this status? + Account *gtsmodel.Account `bun:"rel:belongs-to"` // account corresponding to accountID + AccountURI string `bun:",nullzero,notnull"` // activitypub uri of the owner of this status + InReplyToID string `bun:"type:CHAR(26),nullzero"` // id of the status this status replies to + InReplyToURI string `bun:",nullzero"` // activitypub uri of the status this status is a reply to + InReplyToAccountID string `bun:"type:CHAR(26),nullzero"` // id of the account that this status replies to + InReplyTo *Status `bun:"-"` // status corresponding to inReplyToID + InReplyToAccount *gtsmodel.Account `bun:"rel:belongs-to"` // account corresponding to inReplyToAccountID + BoostOfID string `bun:"type:CHAR(26),nullzero"` // id of the status this status is a boost of + BoostOfURI string `bun:"-"` // URI of the status this status is a boost of; field not inserted in the db, just for dereferencing purposes. + BoostOfAccountID string `bun:"type:CHAR(26),nullzero"` // id of the account that owns the boosted status + BoostOf *Status `bun:"-"` // status that corresponds to boostOfID + BoostOfAccount *gtsmodel.Account `bun:"rel:belongs-to"` // account that corresponds to boostOfAccountID + ThreadID string `bun:"type:CHAR(26),nullzero"` // id of the thread to which this status belongs; only set for remote statuses if a local account is involved at some point in the thread, otherwise null + PollID string `bun:"type:CHAR(26),nullzero"` // + Poll *gtsmodel.Poll `bun:"-"` // + ContentWarning string `bun:",nullzero"` // cw string for this status + Visibility Visibility `bun:",nullzero,notnull"` // visibility entry for this status + Sensitive *bool `bun:",nullzero,notnull,default:false"` // mark the status as sensitive? + Language string `bun:",nullzero"` // what language is this status written in? + CreatedWithApplicationID string `bun:"type:CHAR(26),nullzero"` // Which application was used to create this status? + CreatedWithApplication *gtsmodel.Application `bun:"rel:belongs-to"` // application corresponding to createdWithApplicationID + ActivityStreamsType string `bun:",nullzero,notnull"` // What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types. Will probably almost always be Note but who knows!. + Text string `bun:""` // Original text of the status without formatting + Federated *bool `bun:",notnull"` // This status will be federated beyond the local timeline(s) + InteractionPolicy *gtsmodel.InteractionPolicy `bun:""` // InteractionPolicy for this status. If null then the default InteractionPolicy should be assumed for this status's Visibility. Always null for boost wrappers. + PendingApproval *bool `bun:",nullzero,notnull,default:false"` // If true then status is a reply or boost wrapper that must be Approved by the reply-ee or boost-ee before being fully distributed. + PreApproved bool `bun:"-"` // If true, then status is a reply to or boost wrapper of a status on our instance, has permission to do the interaction, and an Accept should be sent out for it immediately. Field not stored in the DB. + ApprovedByURI string `bun:",nullzero"` // URI of an Accept Activity that approves the Announce or Create Activity that this status was/will be attached to. +} + +// Visibility represents the visibility granularity of a status. +type Visibility string + +const ( + // VisibilityNone means nobody can see this. + // It's only used for web status visibility. + VisibilityNone Visibility = "none" + // VisibilityPublic means this status will be visible to everyone on all timelines. + VisibilityPublic Visibility = "public" + // VisibilityUnlocked means this status will be visible to everyone, but will only show on home timeline to followers, and in lists. + VisibilityUnlocked Visibility = "unlocked" + // VisibilityFollowersOnly means this status is viewable to followers only. + VisibilityFollowersOnly Visibility = "followers_only" + // VisibilityMutualsOnly means this status is visible to mutual followers only. + VisibilityMutualsOnly Visibility = "mutuals_only" + // VisibilityDirect means this status is visible only to mentioned recipients. + VisibilityDirect Visibility = "direct" + // VisibilityDefault is used when no other setting can be found. + VisibilityDefault Visibility = VisibilityUnlocked +) diff --git a/internal/db/bundb/notification.go b/internal/db/bundb/notification.go index ef2527637..a20ab7e3f 100644 --- a/internal/db/bundb/notification.go +++ b/internal/db/bundb/notification.go @@ -196,8 +196,8 @@ func (n *notificationDB) GetAccountNotifications( sinceID string, minID string, limit int, - types []string, - excludeTypes []string, + types []gtsmodel.NotificationType, + excludeTypes []gtsmodel.NotificationType, ) ([]*gtsmodel.Notification, error) { // Ensure reasonable if limit < 0 { @@ -303,7 +303,7 @@ func (n *notificationDB) DeleteNotificationByID(ctx context.Context, id string) return nil } -func (n *notificationDB) DeleteNotifications(ctx context.Context, types []string, targetAccountID string, originAccountID string) error { +func (n *notificationDB) DeleteNotifications(ctx context.Context, types []gtsmodel.NotificationType, targetAccountID string, originAccountID string) error { if targetAccountID == "" && originAccountID == "" { return gtserror.New("one of targetAccountID or originAccountID must be set") } diff --git a/internal/db/bundb/relationship_follow_req.go b/internal/db/bundb/relationship_follow_req.go index 030c99c58..f36d626ca 100644 --- a/internal/db/bundb/relationship_follow_req.go +++ b/internal/db/bundb/relationship_follow_req.go @@ -265,8 +265,8 @@ func (r *relationshipDB) AcceptFollowRequest(ctx context.Context, sourceAccountI } // Delete original follow request notification - if err := r.state.DB.DeleteNotifications(ctx, []string{ - string(gtsmodel.NotificationFollowRequest), + if err := r.state.DB.DeleteNotifications(ctx, []gtsmodel.NotificationType{ + gtsmodel.NotificationFollowRequest, }, targetAccountID, sourceAccountID); err != nil { return nil, err } @@ -281,8 +281,8 @@ func (r *relationshipDB) RejectFollowRequest(ctx context.Context, sourceAccountI } // Delete follow request notification - return r.state.DB.DeleteNotifications(ctx, []string{ - string(gtsmodel.NotificationFollowRequest), + return r.state.DB.DeleteNotifications(ctx, []gtsmodel.NotificationType{ + gtsmodel.NotificationFollowRequest, }, targetAccountID, sourceAccountID) } diff --git a/internal/db/notification.go b/internal/db/notification.go index deb58835a..c962023be 100644 --- a/internal/db/notification.go +++ b/internal/db/notification.go @@ -29,7 +29,7 @@ type Notification interface { // // Returned notifications will be ordered ID descending (ie., highest/newest to lowest/oldest). // If types is empty, *all* notification types will be included. - GetAccountNotifications(ctx context.Context, accountID string, maxID string, sinceID string, minID string, limit int, types []string, excludeTypes []string) ([]*gtsmodel.Notification, error) + GetAccountNotifications(ctx context.Context, accountID string, maxID string, sinceID string, minID string, limit int, types []gtsmodel.NotificationType, excludeTypes []gtsmodel.NotificationType) ([]*gtsmodel.Notification, error) // GetNotificationByID returns one notification according to its id. GetNotificationByID(ctx context.Context, id string) (*gtsmodel.Notification, error) @@ -64,7 +64,7 @@ type Notification interface { // originate from originAccountID will be deleted. // // At least one parameter must not be an empty string. - DeleteNotifications(ctx context.Context, types []string, targetAccountID string, originAccountID string) error + DeleteNotifications(ctx context.Context, types []gtsmodel.NotificationType, targetAccountID string, originAccountID string) error // DeleteNotificationsForStatus deletes all notifications that relate to // the given statusID. This function is useful when a status has been deleted, diff --git a/internal/gtsmodel/accountsettings.go b/internal/gtsmodel/accountsettings.go index 3151ba5b7..4624aa0b1 100644 --- a/internal/gtsmodel/accountsettings.go +++ b/internal/gtsmodel/accountsettings.go @@ -26,7 +26,7 @@ type AccountSettings struct { AccountID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // AccountID that owns this settings. CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created. UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item was last updated. - Privacy Visibility `bun:",nullzero"` // Default post privacy for this account + Privacy Visibility `bun:",nullzero,default:3"` // Default post privacy for this account Sensitive *bool `bun:",nullzero,notnull,default:false"` // Set posts from this account to sensitive by default? Language string `bun:",nullzero,notnull,default:'en'"` // What language does this account post in? StatusContentType string `bun:",nullzero"` // What is the default format for statuses posted by this account (only for local accounts). @@ -34,7 +34,7 @@ type AccountSettings struct { CustomCSS string `bun:",nullzero"` // Custom CSS that should be displayed for this Account's profile and statuses. EnableRSS *bool `bun:",nullzero,notnull,default:false"` // enable RSS feed subscription for this account's public posts at [URL]/feed HideCollections *bool `bun:",nullzero,notnull,default:false"` // Hide this account's followers/following collections. - WebVisibility Visibility `bun:",nullzero,notnull,default:public"` // Visibility level of statuses that visitors can view via the web profile. + WebVisibility Visibility `bun:",nullzero,notnull,default:3"` // Visibility level of statuses that visitors can view via the web profile. InteractionPolicyDirect *InteractionPolicy `bun:""` // Interaction policy to use for new direct visibility statuses by this account. If null, assume default policy. InteractionPolicyMutualsOnly *InteractionPolicy `bun:""` // Interaction policy to use for new mutuals only visibility statuses. If null, assume default policy. InteractionPolicyFollowersOnly *InteractionPolicy `bun:""` // Interaction policy to use for new followers only visibility statuses. If null, assume default policy. diff --git a/internal/gtsmodel/common.go b/internal/gtsmodel/common.go new file mode 100644 index 000000000..e740bbb81 --- /dev/null +++ b/internal/gtsmodel/common.go @@ -0,0 +1,24 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package gtsmodel + +// enumType is the type we (at least, should) use +// for database enum types. it is the largest size +// supported by a PostgreSQL SMALLINT, since an +// SQLite SMALLINT is actually variable in size. +type enumType int16 diff --git a/internal/gtsmodel/interactionpolicy.go b/internal/gtsmodel/interactionpolicy.go index d8d890e69..7fcafc80d 100644 --- a/internal/gtsmodel/interactionpolicy.go +++ b/internal/gtsmodel/interactionpolicy.go @@ -224,7 +224,7 @@ func DefaultInteractionPolicyFor(v Visibility) *InteractionPolicy { case VisibilityDirect: return DefaultInteractionPolicyDirect() default: - panic("visibility " + v + " not recognized") + panic("invalid visibility") } } diff --git a/internal/gtsmodel/notification.go b/internal/gtsmodel/notification.go index 5cf6b061a..49f1fe2bb 100644 --- a/internal/gtsmodel/notification.go +++ b/internal/gtsmodel/notification.go @@ -34,20 +34,51 @@ type Notification struct { Read *bool `bun:",nullzero,notnull,default:false"` // Notification has been seen/read } -// NotificationType describes the reason/type of this notification. -type NotificationType string +// NotificationType describes the +// reason/type of this notification. +type NotificationType enumType -// Notification Types const ( - NotificationFollow NotificationType = "follow" // NotificationFollow -- someone followed you - NotificationFollowRequest NotificationType = "follow_request" // NotificationFollowRequest -- someone requested to follow you - NotificationMention NotificationType = "mention" // NotificationMention -- someone mentioned you in their status - NotificationReblog NotificationType = "reblog" // NotificationReblog -- someone boosted one of your statuses - NotificationFave NotificationType = "favourite" // NotificationFave -- someone faved/liked one of your statuses - NotificationPoll NotificationType = "poll" // NotificationPoll -- a poll you voted in or created has ended - NotificationStatus NotificationType = "status" // NotificationStatus -- someone you enabled notifications for has posted a status. - NotificationSignup NotificationType = "admin.sign_up" // NotificationSignup -- someone has submitted a new account sign-up to the instance. - NotificationPendingFave NotificationType = "pending.favourite" // Someone has faved a status of yours, which requires approval by you. - NotificationPendingReply NotificationType = "pending.reply" // Someone has replied to a status of yours, which requires approval by you. - NotificationPendingReblog NotificationType = "pending.reblog" // Someone has boosted a status of yours, which requires approval by you. + // Notification Types + NotificationFollow NotificationType = 1 // NotificationFollow -- someone followed you + NotificationFollowRequest NotificationType = 2 // NotificationFollowRequest -- someone requested to follow you + NotificationMention NotificationType = 3 // NotificationMention -- someone mentioned you in their status + NotificationReblog NotificationType = 4 // NotificationReblog -- someone boosted one of your statuses + NotificationFave NotificationType = 5 // NotificationFave -- someone faved/liked one of your statuses + NotificationPoll NotificationType = 6 // NotificationPoll -- a poll you voted in or created has ended + NotificationStatus NotificationType = 7 // NotificationStatus -- someone you enabled notifications for has posted a status. + NotificationSignup NotificationType = 8 // NotificationSignup -- someone has submitted a new account sign-up to the instance. + NotificationPendingFave NotificationType = 9 // Someone has faved a status of yours, which requires approval by you. + NotificationPendingReply NotificationType = 10 // Someone has replied to a status of yours, which requires approval by you. + NotificationPendingReblog NotificationType = 11 // Someone has boosted a status of yours, which requires approval by you. ) + +// String returns a stringified, frontend API compatible form of NotificationType. +func (t NotificationType) String() string { + switch t { + case NotificationFollow: + return "follow" + case NotificationFollowRequest: + return "follow_request" + case NotificationMention: + return "mention" + case NotificationReblog: + return "reblog" + case NotificationFave: + return "favourite" + case NotificationPoll: + return "poll" + case NotificationStatus: + return "status" + case NotificationSignup: + return "admin.sign_up" + case NotificationPendingFave: + return "pending.favourite" + case NotificationPendingReply: + return "pending.reply" + case NotificationPendingReblog: + return "pending.reblog" + default: + panic("invalid notification type") + } +} diff --git a/internal/gtsmodel/status.go b/internal/gtsmodel/status.go index 91c0ada61..f8bd068ab 100644 --- a/internal/gtsmodel/status.go +++ b/internal/gtsmodel/status.go @@ -263,27 +263,58 @@ type StatusToEmoji struct { Emoji *Emoji `bun:"rel:belongs-to"` } -// Visibility represents the visibility granularity of a status. -type Visibility string +// Visibility represents the +// visibility granularity of a status. +type Visibility enumType const ( // VisibilityNone means nobody can see this. // It's only used for web status visibility. - VisibilityNone Visibility = "none" - // VisibilityPublic means this status will be visible to everyone on all timelines. - VisibilityPublic Visibility = "public" - // VisibilityUnlocked means this status will be visible to everyone, but will only show on home timeline to followers, and in lists. - VisibilityUnlocked Visibility = "unlocked" + VisibilityNone Visibility = 1 + + // VisibilityPublic means this status will + // be visible to everyone on all timelines. + VisibilityPublic Visibility = 2 + + // VisibilityUnlocked means this status will be visible to everyone, + // but will only show on home timeline to followers, and in lists. + VisibilityUnlocked Visibility = 3 + // VisibilityFollowersOnly means this status is viewable to followers only. - VisibilityFollowersOnly Visibility = "followers_only" - // VisibilityMutualsOnly means this status is visible to mutual followers only. - VisibilityMutualsOnly Visibility = "mutuals_only" - // VisibilityDirect means this status is visible only to mentioned recipients. - VisibilityDirect Visibility = "direct" + VisibilityFollowersOnly Visibility = 4 + + // VisibilityMutualsOnly means this status + // is visible to mutual followers only. + VisibilityMutualsOnly Visibility = 5 + + // VisibilityDirect means this status is + // visible only to mentioned recipients. + VisibilityDirect Visibility = 6 + // VisibilityDefault is used when no other setting can be found. VisibilityDefault Visibility = VisibilityUnlocked ) +// String returns a stringified, frontend API compatible form of Visibility. +func (v Visibility) String() string { + switch v { + case VisibilityNone: + return "none" + case VisibilityPublic: + return "public" + case VisibilityUnlocked: + return "unlocked" + case VisibilityFollowersOnly: + return "followers_only" + case VisibilityMutualsOnly: + return "mutuals_only" + case VisibilityDirect: + return "direct" + default: + panic("invalid visibility") + } +} + // Content models the simple string content // of a status along with its ContentMap, // which contains content entries keyed by diff --git a/internal/processing/preferences.go b/internal/processing/preferences.go index 0a5f566ae..fb445ec5b 100644 --- a/internal/processing/preferences.go +++ b/internal/processing/preferences.go @@ -46,7 +46,7 @@ func (p *Processor) PreferencesGet(ctx context.Context, accountID string) (*apim func mastoPrefVisibility(vis gtsmodel.Visibility) string { switch vis { case gtsmodel.VisibilityPublic, gtsmodel.VisibilityDirect: - return string(vis) + return vis.String() case gtsmodel.VisibilityUnlocked: return "unlisted" default: diff --git a/internal/processing/status/create.go b/internal/processing/status/create.go index fbc2dadf7..ef8f8aa56 100644 --- a/internal/processing/status/create.go +++ b/internal/processing/status/create.go @@ -372,7 +372,7 @@ func (p *Processor) processVisibility( // Fall back to account default, set // this back on the form for later use. - case accountDefaultVis != "": + case accountDefaultVis != 0: status.Visibility = accountDefaultVis form.Visibility = p.converter.VisToAPIVis(ctx, accountDefaultVis) diff --git a/internal/processing/timeline/notification.go b/internal/processing/timeline/notification.go index 34e6d865d..92dbf851f 100644 --- a/internal/processing/timeline/notification.go +++ b/internal/processing/timeline/notification.go @@ -41,8 +41,8 @@ func (p *Processor) NotificationsGet( sinceID string, minID string, limit int, - types []string, - excludeTypes []string, + types []gtsmodel.NotificationType, + excludeTypes []gtsmodel.NotificationType, ) (*apimodel.PageableResponse, gtserror.WithCode) { notifs, err := p.state.DB.GetAccountNotifications( ctx, diff --git a/internal/processing/workers/surfacenotify.go b/internal/processing/workers/surfacenotify.go index 872ccca65..1520d2ec0 100644 --- a/internal/processing/workers/surfacenotify.go +++ b/internal/processing/workers/surfacenotify.go @@ -542,7 +542,7 @@ func getNotifyLockURI( ) string { builder := strings.Builder{} builder.WriteString("notification:?") - builder.WriteString("type=" + string(notificationType)) + builder.WriteString("type=" + notificationType.String()) builder.WriteString("&target=" + targetAccount.URI) builder.WriteString("&origin=" + originAccount.URI) if statusID != "" { diff --git a/internal/typeutils/frontendtointernal.go b/internal/typeutils/frontendtointernal.go index 1f7d1877e..82957ee05 100644 --- a/internal/typeutils/frontendtointernal.go +++ b/internal/typeutils/frontendtointernal.go @@ -41,7 +41,7 @@ func APIVisToVis(m apimodel.Visibility) gtsmodel.Visibility { case apimodel.VisibilityNone: return gtsmodel.VisibilityNone } - return "" + return 0 } func APIMarkerNameToMarkerName(m apimodel.MarkerName) gtsmodel.MarkerName { diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 5f919f014..750d4eec4 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -1862,7 +1862,7 @@ func (c *Converter) NotificationToAPINotification( return &apimodel.Notification{ ID: n.ID, - Type: string(n.NotificationType), + Type: n.NotificationType.String(), CreatedAt: util.FormatISO8601(n.CreatedAt), Account: apiAccount, Status: apiStatus, From da4db81bcf1a66d0de559015e061e602d8f2fcb8 Mon Sep 17 00:00:00 2001 From: VirtualWolf Date: Tue, 26 Nov 2024 00:50:41 +1100 Subject: [PATCH 05/26] [docs] Added note to documentation about mutuals-only posts not being functional. (#3557) --- docs/user_guide/posts.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/user_guide/posts.md b/docs/user_guide/posts.md index 6e2f57e6f..1f718cfae 100644 --- a/docs/user_guide/posts.md +++ b/docs/user_guide/posts.md @@ -36,6 +36,9 @@ Direct posts are **not** accessible via a web URL on your GoToSocial instance. ### Mutuals-only +!!! warning + Mutuals-only posts are not currently functional. + Posts with a visibility of `mutuals_only` will only appear to the post author, and to *mutual follows* of the post author. In other words, they can only be seen by others if two conditions are met: 1. The other account follows the post author. From c454b1b4882389122964c75d7d764b51312743f7 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:15:33 +0100 Subject: [PATCH 06/26] [chore] Bump tooling versions, bump go -> v1.23.0 (#3258) * [chore] Bump tooling versions, bump go -> v1.23.0 * undo silly change * sign * bump go version in go.mod * allow overflow in imaging * goreleaser deprecation notices * bump versions * undo accidental rebase change * update container versions to just use latest major version * update swagger to our release with go1.23 fix * update goreleaser to use our vendored swagger version --------- Co-authored-by: kim --- .drone.yml | 12 ++++++------ .goreleaser.yml | 5 +++-- Dockerfile | 4 ++-- go.mod | 5 ++++- go.sum | 6 ++---- internal/db/bundb/bundb.go | 11 +++++------ test/swagger.sh | 2 +- .../go-swagger/go-swagger/codescan/schema.go | 2 ++ vendor/modules.txt | 3 ++- 9 files changed, 27 insertions(+), 23 deletions(-) diff --git a/.drone.yml b/.drone.yml index 589c20b24..5b954876d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -12,7 +12,7 @@ steps: # We use golangci-lint for linting. # See: https://golangci-lint.run/ - name: lint - image: golangci/golangci-lint:v1.57.2 + image: golangci/golangci-lint:v1.62.0 volumes: - name: go-build-cache path: /root/.cache/go-build @@ -28,7 +28,7 @@ steps: - pull_request - name: test - image: golang:1.22-alpine + image: golang:1.23-alpine volumes: - name: go-build-cache path: /root/.cache/go-build @@ -94,7 +94,7 @@ steps: - pull_request - name: snapshot - image: superseriousbusiness/gotosocial-drone-build:0.6.2 # https://github.com/superseriousbusiness/gotosocial-drone-build + image: superseriousbusiness/gotosocial-drone-build:0.7.0 # https://github.com/superseriousbusiness/gotosocial-drone-build volumes: - name: go-build-cache path: /root/.cache/go-build @@ -141,7 +141,7 @@ steps: - main - name: release - image: superseriousbusiness/gotosocial-drone-build:0.6.2 # https://github.com/superseriousbusiness/gotosocial-drone-build + image: superseriousbusiness/gotosocial-drone-build:0.7.0 # https://github.com/superseriousbusiness/gotosocial-drone-build volumes: - name: go-build-cache path: /root/.cache/go-build @@ -210,7 +210,7 @@ clone: steps: - name: mirror - image: superseriousbusiness/gotosocial-drone-build:0.6.2 + image: superseriousbusiness/gotosocial-drone-build:0.7.0 environment: ORIGIN_REPO: https://github.com/superseriousbusiness/gotosocial TARGET_REPO: https://codeberg.org/superseriousbusiness/gotosocial @@ -223,6 +223,6 @@ steps: --- kind: signature -hmac: 1b89e3a538fbca72eb9a0b398cd82f09a774ba3649013e19d36012eda327e83f +hmac: 9810bf692fb1029c13b0a1e2f556e2306d16f7d3eec9ca6163a0499c147280c1 ... diff --git a/.goreleaser.yml b/.goreleaser.yml index 8b4589395..c1c170c5a 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,4 +1,5 @@ -# https://goreleaser.com +# Version 2 of GoReleaser: https://goreleaser.com/errors/version/ +version: 2 project_name: gotosocial version: 2 @@ -6,7 +7,7 @@ version: 2 before: hooks: # generate the swagger.yaml file using go-swagger and bundle it into the assets directory - - swagger generate spec --scan-models --exclude-deps -o web/assets/swagger.yaml + - go run ./vendor/github.com/go-swagger/go-swagger/cmd/swagger generate spec --scan-models --exclude-deps -o web/assets/swagger.yaml - sed -i "s/REPLACE_ME/{{ incpatch .Version }}/" web/assets/swagger.yaml # Install web deps + bundle web assets - yarn --cwd ./web/source install diff --git a/Dockerfile b/Dockerfile index df4fc56da..7b7728a53 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # Dockerfile reference: https://docs.docker.com/engine/reference/builder/ # stage 1: generate up-to-date swagger.yaml to put in the final container -FROM --platform=${BUILDPLATFORM} golang:1.22-alpine AS swagger +FROM --platform=${BUILDPLATFORM} golang:1.23-alpine AS swagger RUN \ ### Installs goswagger for building swagger definitions inside this container @@ -28,7 +28,7 @@ RUN yarn --cwd ./web/source install && \ rm -rf ./web/source # stage 3: build the executor container -FROM --platform=${TARGETPLATFORM} alpine:3.19.1 as executor +FROM --platform=${TARGETPLATFORM} alpine:3.20 as executor # switch to non-root user:group for GtS USER 1000:1000 diff --git a/go.mod b/go.mod index bc1d56a46..e95aecc68 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,9 @@ module github.com/superseriousbusiness/gotosocial -go 1.22.2 +go 1.23 + +// Replace go-swagger with our version that fixes (ours particularly) use of Go1.23 +replace github.com/go-swagger/go-swagger => github.com/superseriousbusiness/go-swagger v0.31.0-gts-go1.23-fix // Replace modernc/sqlite with our version that fixes the concurrency INTERRUPT issue replace modernc.org/sqlite => gitlab.com/NyaaaWhatsUpDoc/sqlite v1.33.1-concurrency-workaround diff --git a/go.sum b/go.sum index 78d023286..3ce2e8853 100644 --- a/go.sum +++ b/go.sum @@ -238,8 +238,6 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91 github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0= -github.com/go-swagger/go-swagger v0.31.0 h1:H8eOYQnY2u7vNKWDNykv2xJP3pBhRG/R+SOCAmKrLlc= -github.com/go-swagger/go-swagger v0.31.0/go.mod h1:WSigRRWEig8zV6t6Sm8Y+EmUjlzA/HoaZJ5edupq7po= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b h1:khEcpUM4yFcxg4/FHQWkvVRmgijNXRfzkIDHh23ggEo= @@ -330,8 +328,6 @@ github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pw github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.2 h1:qoW6V1GT3aZxybsbC6oLnailWnB+qTMVwMreOso9XUw= -github.com/gorilla/websocket v1.5.2/go.mod h1:0n9H61RBAcf5/38py2MCYbxzPIY9rOkpvvMT24Rqs30= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= @@ -538,6 +534,8 @@ github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430 github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe/go.mod h1:gH4P6gN1V+wmIw5o97KGaa1RgXB/tVpC2UNzijhg3E4= github.com/superseriousbusiness/go-png-image-structure/v2 v2.0.1-SSB h1:8psprYSK1KdOSH7yQ4PbJq0YYaGQY+gzdW/B0ExDb/8= github.com/superseriousbusiness/go-png-image-structure/v2 v2.0.1-SSB/go.mod h1:ymKGfy9kg4dIdraeZRAdobMS/flzLk3VcRPLpEWOAXg= +github.com/superseriousbusiness/go-swagger v0.31.0-gts-go1.23-fix h1:CXcjArOyxBPFgsNAu4As+RK9BwOUEG1LL7ja4g7iax0= +github.com/superseriousbusiness/go-swagger v0.31.0-gts-go1.23-fix/go.mod h1:WSigRRWEig8zV6t6Sm8Y+EmUjlzA/HoaZJ5edupq7po= github.com/superseriousbusiness/httpsig v1.2.0-SSB h1:BinBGKbf2LSuVT5+MuH0XynHN9f0XVshx2CTDtkaWj0= github.com/superseriousbusiness/httpsig v1.2.0-SSB/go.mod h1:+rxfATjFaDoDIVaJOTSP0gj6UrbicaYPEptvCLC9F28= github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8 h1:nTIhuP157oOFcscuoK1kCme1xTeGIzztSw70lX9NrDQ= diff --git a/internal/db/bundb/bundb.go b/internal/db/bundb/bundb.go index d10f372fd..8756e086b 100644 --- a/internal/db/bundb/bundb.go +++ b/internal/db/bundb/bundb.go @@ -420,13 +420,12 @@ func maxOpenConns() int { // deriveBunDBPGOptions takes an application config and returns either a ready-to-use set of options // with sensible defaults, or an error if it's not satisfied by the provided config. func deriveBunDBPGOptions() (*pgx.ConnConfig, error) { - url := config.GetDbPostgresConnectionString() - - // if database URL is defined, ignore other DB related configuration fields - if url != "" { - cfg, err := pgx.ParseConfig(url) - return cfg, err + // If database URL is defined, ignore + // other DB-related configuration fields. + if url := config.GetDbPostgresConnectionString(); url != "" { + return pgx.ParseConfig(url) } + // these are all optional, the db adapter figures out defaults address := config.GetDbAddress() diff --git a/test/swagger.sh b/test/swagger.sh index e8b4b5864..c7644c1af 100755 --- a/test/swagger.sh +++ b/test/swagger.sh @@ -5,7 +5,7 @@ set -eu swagger_cmd() { - go run github.com/go-swagger/go-swagger/cmd/swagger "$@" + go run ./vendor/github.com/go-swagger/go-swagger/cmd/swagger "$@" } swagger_spec='docs/api/swagger.yaml' diff --git a/vendor/github.com/go-swagger/go-swagger/codescan/schema.go b/vendor/github.com/go-swagger/go-swagger/codescan/schema.go index dce74fc30..98bdecba6 100644 --- a/vendor/github.com/go-swagger/go-swagger/codescan/schema.go +++ b/vendor/github.com/go-swagger/go-swagger/codescan/schema.go @@ -303,6 +303,8 @@ func (s *schemaBuilder) buildFromType(tpe types.Type, tgt swaggerTypable) error } switch titpe := tpe.(type) { + case *types.Alias: + return nil // resolves panic case *types.Basic: return swaggerSchemaForType(titpe.String(), tgt) case *types.Pointer: diff --git a/vendor/modules.txt b/vendor/modules.txt index 8af740067..eb965e0cb 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -300,7 +300,7 @@ github.com/go-playground/universal-translator # github.com/go-playground/validator/v10 v10.20.0 ## explicit; go 1.18 github.com/go-playground/validator/v10 -# github.com/go-swagger/go-swagger v0.31.0 +# github.com/go-swagger/go-swagger v0.31.0 => github.com/superseriousbusiness/go-swagger v0.31.0-gts-go1.23-fix ## explicit; go 1.21 github.com/go-swagger/go-swagger/cmd/swagger github.com/go-swagger/go-swagger/cmd/swagger/commands @@ -1344,6 +1344,7 @@ modernc.org/token # mvdan.cc/xurls/v2 v2.5.0 ## explicit; go 1.19 mvdan.cc/xurls/v2 +# github.com/go-swagger/go-swagger => github.com/superseriousbusiness/go-swagger v0.31.0-gts-go1.23-fix # modernc.org/sqlite => gitlab.com/NyaaaWhatsUpDoc/sqlite v1.33.1-concurrency-workaround # go.opentelemetry.io/otel => go.opentelemetry.io/otel v1.29.0 # go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 From a444adee979375ed5d7af38346029a3d90bc77eb Mon Sep 17 00:00:00 2001 From: kim <89579420+NyaaaWhatsUpDoc@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:33:21 +0000 Subject: [PATCH 07/26] [bugfix] notification types missing from link header (#3571) * ensure notification types get included in link header query for notifications * fix type query keys --- .../client/notifications/notificationsget.go | 30 ++++------ internal/db/bundb/notification.go | 53 +++++++----------- internal/db/bundb/notification_test.go | 46 ++++++++------- internal/db/notification.go | 3 +- internal/processing/timeline/notification.go | 56 +++++++++---------- .../processing/workers/surfacenotify_test.go | 2 +- 6 files changed, 87 insertions(+), 103 deletions(-) diff --git a/internal/api/client/notifications/notificationsget.go b/internal/api/client/notifications/notificationsget.go index cc3e5bdb7..841768c63 100644 --- a/internal/api/client/notifications/notificationsget.go +++ b/internal/api/client/notifications/notificationsget.go @@ -18,14 +18,13 @@ package notifications import ( - "fmt" "net/http" - "strconv" "github.com/gin-gonic/gin" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/internal/paging" ) // NotificationsGETHandler swagger:operation GET /api/v1/notifications notifications @@ -152,18 +151,6 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) { return } - limit := 20 - limitString := c.Query(LimitKey) - if limitString != "" { - i, err := strconv.ParseInt(limitString, 10, 32) - if err != nil { - err := fmt.Errorf("error parsing %s: %s", LimitKey, err) - apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) - return - } - limit = int(i) - } - types, errWithCode := apiutil.ParseNotificationTypes(c.QueryArray(TypesKey)) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) @@ -176,13 +163,20 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) { return } + page, errWithCode := paging.ParseIDPage(c, + 1, // min limit + 80, // max limit + 20, // no limit + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + resp, errWithCode := m.processor.Timeline().NotificationsGet( c.Request.Context(), authed, - c.Query(MaxIDKey), - c.Query(SinceIDKey), - c.Query(MinIDKey), - limit, + page, types, exclTypes, ) diff --git a/internal/db/bundb/notification.go b/internal/db/bundb/notification.go index a20ab7e3f..d4f8799bd 100644 --- a/internal/db/bundb/notification.go +++ b/internal/db/bundb/notification.go @@ -26,8 +26,8 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtscontext" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/log" + "github.com/superseriousbusiness/gotosocial/internal/paging" "github.com/superseriousbusiness/gotosocial/internal/state" "github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/uptrace/bun" @@ -192,22 +192,19 @@ func (n *notificationDB) PopulateNotification(ctx context.Context, notif *gtsmod func (n *notificationDB) GetAccountNotifications( ctx context.Context, accountID string, - maxID string, - sinceID string, - minID string, - limit int, + page *paging.Page, types []gtsmodel.NotificationType, excludeTypes []gtsmodel.NotificationType, ) ([]*gtsmodel.Notification, error) { - // Ensure reasonable - if limit < 0 { - limit = 0 - } - - // Make educated guess for slice size var ( - notifIDs = make([]string, 0, limit) - frontToBack = true + // Get paging params. + minID = page.GetMin() + maxID = page.GetMax() + limit = page.GetLimit() + order = page.GetOrder() + + // Make educated guess for slice size + notifIDs = make([]string, 0, limit) ) q := n.db. @@ -215,23 +212,14 @@ func (n *notificationDB) GetAccountNotifications( TableExpr("? AS ?", bun.Ident("notifications"), bun.Ident("notification")). Column("notification.id") - if maxID == "" { - maxID = id.Highest - } - - // Return only notifs LOWER (ie., older) than maxID. - q = q.Where("? < ?", bun.Ident("notification.id"), maxID) - - if sinceID != "" { - // Return only notifs HIGHER (ie., newer) than sinceID. - q = q.Where("? > ?", bun.Ident("notification.id"), sinceID) + if maxID != "" { + // Return only notifs LOWER (ie., older) than maxID. + q = q.Where("? < ?", bun.Ident("notification.id"), maxID) } if minID != "" { // Return only notifs HIGHER (ie., newer) than minID. q = q.Where("? > ?", bun.Ident("notification.id"), minID) - - frontToBack = false // page up } if len(types) > 0 { @@ -251,12 +239,12 @@ func (n *notificationDB) GetAccountNotifications( q = q.Limit(limit) } - if frontToBack { - // Page down. - q = q.Order("notification.id DESC") - } else { + if order == paging.OrderAscending { // Page up. q = q.Order("notification.id ASC") + } else { + // Page down. + q = q.Order("notification.id DESC") } if err := q.Scan(ctx, ¬ifIDs); err != nil { @@ -269,11 +257,8 @@ func (n *notificationDB) GetAccountNotifications( // If we're paging up, we still want notifications // to be sorted by ID desc, so reverse ids slice. - // https://zchee.github.io/golang-wiki/SliceTricks/#reversing - if !frontToBack { - for l, r := 0, len(notifIDs)-1; l < r; l, r = l+1, r-1 { - notifIDs[l], notifIDs[r] = notifIDs[r], notifIDs[l] - } + if order == paging.OrderAscending { + slices.Reverse(notifIDs) } // Fetch notification models by their IDs. diff --git a/internal/db/bundb/notification_test.go b/internal/db/bundb/notification_test.go index eb2c02066..8e2fb8031 100644 --- a/internal/db/bundb/notification_test.go +++ b/internal/db/bundb/notification_test.go @@ -28,6 +28,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtscontext" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/paging" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -92,10 +93,11 @@ func (suite *NotificationTestSuite) TestGetAccountNotificationsWithSpam() { notifications, err := suite.db.GetAccountNotifications( gtscontext.SetBarebones(context.Background()), testAccount.ID, - id.Highest, - id.Lowest, - "", - 20, + &paging.Page{ + Min: paging.EitherMinID("", id.Lowest), + Max: paging.MaxID(id.Highest), + Limit: 20, + }, nil, nil, ) @@ -115,10 +117,11 @@ func (suite *NotificationTestSuite) TestGetAccountNotificationsWithoutSpam() { notifications, err := suite.db.GetAccountNotifications( gtscontext.SetBarebones(context.Background()), testAccount.ID, - id.Highest, - id.Lowest, - "", - 20, + &paging.Page{ + Min: paging.EitherMinID("", id.Lowest), + Max: paging.MaxID(id.Highest), + Limit: 20, + }, nil, nil, ) @@ -140,10 +143,11 @@ func (suite *NotificationTestSuite) TestDeleteNotificationsWithSpam() { notifications, err := suite.db.GetAccountNotifications( gtscontext.SetBarebones(context.Background()), testAccount.ID, - id.Highest, - id.Lowest, - "", - 20, + &paging.Page{ + Min: paging.EitherMinID("", id.Lowest), + Max: paging.MaxID(id.Highest), + Limit: 20, + }, nil, nil, ) @@ -161,10 +165,11 @@ func (suite *NotificationTestSuite) TestDeleteNotificationsWithSpam() { notifications, err = suite.db.GetAccountNotifications( gtscontext.SetBarebones(context.Background()), testAccount.ID, - id.Highest, - id.Lowest, - "", - 20, + &paging.Page{ + Min: paging.EitherMinID("", id.Lowest), + Max: paging.MaxID(id.Highest), + Limit: 20, + }, nil, nil, ) @@ -183,10 +188,11 @@ func (suite *NotificationTestSuite) TestDeleteNotificationsWithTwoAccounts() { notifications, err := suite.db.GetAccountNotifications( gtscontext.SetBarebones(context.Background()), testAccount.ID, - id.Highest, - id.Lowest, - "", - 20, + &paging.Page{ + Min: paging.EitherMinID("", id.Lowest), + Max: paging.MaxID(id.Highest), + Limit: 20, + }, nil, nil, ) diff --git a/internal/db/notification.go b/internal/db/notification.go index c962023be..c608261dc 100644 --- a/internal/db/notification.go +++ b/internal/db/notification.go @@ -21,6 +21,7 @@ import ( "context" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/paging" ) // Notification contains functions for creating and getting notifications. @@ -29,7 +30,7 @@ type Notification interface { // // Returned notifications will be ordered ID descending (ie., highest/newest to lowest/oldest). // If types is empty, *all* notification types will be included. - GetAccountNotifications(ctx context.Context, accountID string, maxID string, sinceID string, minID string, limit int, types []gtsmodel.NotificationType, excludeTypes []gtsmodel.NotificationType) ([]*gtsmodel.Notification, error) + GetAccountNotifications(ctx context.Context, accountID string, page *paging.Page, types []gtsmodel.NotificationType, excludeTypes []gtsmodel.NotificationType) ([]*gtsmodel.Notification, error) // GetNotificationByID returns one notification according to its id. GetNotificationByID(ctx context.Context, id string) (*gtsmodel.Notification, error) diff --git a/internal/processing/timeline/notification.go b/internal/processing/timeline/notification.go index 92dbf851f..a242c7b74 100644 --- a/internal/processing/timeline/notification.go +++ b/internal/processing/timeline/notification.go @@ -21,6 +21,7 @@ import ( "context" "errors" "fmt" + "net/url" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" @@ -31,26 +32,21 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/internal/paging" "github.com/superseriousbusiness/gotosocial/internal/util" ) func (p *Processor) NotificationsGet( ctx context.Context, authed *oauth.Auth, - maxID string, - sinceID string, - minID string, - limit int, + page *paging.Page, types []gtsmodel.NotificationType, excludeTypes []gtsmodel.NotificationType, ) (*apimodel.PageableResponse, gtserror.WithCode) { notifs, err := p.state.DB.GetAccountNotifications( ctx, authed.Account.ID, - maxID, - sinceID, - minID, - limit, + page, types, excludeTypes, ) @@ -78,22 +74,15 @@ func (p *Processor) NotificationsGet( compiledMutes := usermute.NewCompiledUserMuteList(mutes) var ( - items = make([]interface{}, 0, count) - nextMaxIDValue string - prevMinIDValue string + items = make([]interface{}, 0, count) + + // Get the lowest and highest + // ID values, used for paging. + lo = notifs[count-1].ID + hi = notifs[0].ID ) - for i, n := range notifs { - // Set next + prev values before filtering and API - // converting, so caller can still page properly. - if i == count-1 { - nextMaxIDValue = n.ID - } - - if i == 0 { - prevMinIDValue = n.ID - } - + for _, n := range notifs { visible, err := p.notifVisible(ctx, n, authed.Account) if err != nil { log.Debugf(ctx, "skipping notification %s because of an error checking notification visibility: %v", n.ID, err) @@ -115,13 +104,22 @@ func (p *Processor) NotificationsGet( items = append(items, item) } - return util.PackagePageableResponse(util.PageableResponseParams{ - Items: items, - Path: "api/v1/notifications", - NextMaxIDValue: nextMaxIDValue, - PrevMinIDValue: prevMinIDValue, - Limit: limit, - }) + // Build type query string. + query := make(url.Values) + for _, typ := range types { + query.Add("types[]", typ.String()) + } + for _, typ := range excludeTypes { + query.Add("exclude_types[]", typ.String()) + } + + return paging.PackageResponse(paging.ResponseParams{ + Items: items, + Path: "/api/v1/notifications", + Next: page.Next(lo, hi), + Prev: page.Prev(lo, hi), + Query: query, + }), nil } func (p *Processor) NotificationGet(ctx context.Context, account *gtsmodel.Account, targetNotifID string) (*apimodel.Notification, gtserror.WithCode) { diff --git a/internal/processing/workers/surfacenotify_test.go b/internal/processing/workers/surfacenotify_test.go index dc445d0ac..52ee89e8b 100644 --- a/internal/processing/workers/surfacenotify_test.go +++ b/internal/processing/workers/surfacenotify_test.go @@ -89,7 +89,7 @@ func (suite *SurfaceNotifyTestSuite) TestSpamNotifs() { notifs, err := testStructs.State.DB.GetAccountNotifications( gtscontext.SetBarebones(ctx), targetAccount.ID, - "", "", "", 0, nil, nil, + nil, nil, nil, ) if err != nil { suite.FailNow(err.Error()) From 3fceb5fc1a83a6ba3ca3c314eef50f0b45cd6009 Mon Sep 17 00:00:00 2001 From: kim <89579420+NyaaaWhatsUpDoc@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:42:37 +0000 Subject: [PATCH 08/26] bumps uptrace/bun dependencies to v1.2.6 (#3569) --- go.mod | 11 +- go.sum | 22 +- .../github.com/bahlo/generic-list-go/LICENSE | 27 + .../bahlo/generic-list-go/README.md | 5 + .../github.com/bahlo/generic-list-go/list.go | 235 +++ vendor/github.com/buger/jsonparser/.gitignore | 12 + .../github.com/buger/jsonparser/.travis.yml | 11 + vendor/github.com/buger/jsonparser/Dockerfile | 12 + vendor/github.com/buger/jsonparser/LICENSE | 21 + vendor/github.com/buger/jsonparser/Makefile | 36 + vendor/github.com/buger/jsonparser/README.md | 365 +++++ vendor/github.com/buger/jsonparser/bytes.go | 47 + .../github.com/buger/jsonparser/bytes_safe.go | 25 + .../buger/jsonparser/bytes_unsafe.go | 44 + vendor/github.com/buger/jsonparser/escape.go | 173 +++ vendor/github.com/buger/jsonparser/fuzz.go | 117 ++ .../buger/jsonparser/oss-fuzz-build.sh | 47 + vendor/github.com/buger/jsonparser/parser.go | 1283 +++++++++++++++++ vendor/github.com/uptrace/bun/CHANGELOG.md | 49 + vendor/github.com/uptrace/bun/Makefile | 2 +- vendor/github.com/uptrace/bun/README.md | 1 + vendor/github.com/uptrace/bun/db.go | 3 +- .../github.com/uptrace/bun/dialect/append.go | 18 +- .../uptrace/bun/dialect/feature/feature.go | 5 +- .../bun/dialect/pgdialect/alter_table.go | 245 ++++ .../uptrace/bun/dialect/pgdialect/array.go | 31 +- .../uptrace/bun/dialect/pgdialect/dialect.go | 13 +- .../bun/dialect/pgdialect/inspector.go | 297 ++++ .../uptrace/bun/dialect/pgdialect/sqltype.go | 84 +- .../uptrace/bun/dialect/pgdialect/version.go | 2 +- .../bun/dialect/sqlitedialect/dialect.go | 14 +- .../bun/dialect/sqlitedialect/version.go | 2 +- .../github.com/uptrace/bun/internal/util.go | 6 + vendor/github.com/uptrace/bun/migrate/auto.go | 429 ++++++ vendor/github.com/uptrace/bun/migrate/diff.go | 411 ++++++ .../uptrace/bun/migrate/migrator.go | 23 +- .../uptrace/bun/migrate/operations.go | 340 +++++ .../uptrace/bun/migrate/sqlschema/column.go | 75 + .../uptrace/bun/migrate/sqlschema/database.go | 127 ++ .../bun/migrate/sqlschema/inspector.go | 241 ++++ .../uptrace/bun/migrate/sqlschema/migrator.go | 49 + .../uptrace/bun/migrate/sqlschema/table.go | 60 + .../uptrace/bun/model_table_has_many.go | 11 +- vendor/github.com/uptrace/bun/package.json | 2 +- vendor/github.com/uptrace/bun/query_base.go | 112 ++ .../uptrace/bun/query_column_add.go | 9 +- .../uptrace/bun/query_column_drop.go | 9 +- vendor/github.com/uptrace/bun/query_delete.go | 63 +- vendor/github.com/uptrace/bun/query_insert.go | 10 +- vendor/github.com/uptrace/bun/query_merge.go | 10 +- vendor/github.com/uptrace/bun/query_raw.go | 9 + vendor/github.com/uptrace/bun/query_select.go | 105 +- .../uptrace/bun/query_table_drop.go | 9 + vendor/github.com/uptrace/bun/query_update.go | 49 +- .../github.com/uptrace/bun/schema/dialect.go | 7 + vendor/github.com/uptrace/bun/schema/table.go | 63 +- .../github.com/uptrace/bun/schema/tables.go | 12 + vendor/github.com/uptrace/bun/version.go | 2 +- .../wk8/go-ordered-map/v2/.gitignore | 1 + .../wk8/go-ordered-map/v2/.golangci.yml | 78 + .../wk8/go-ordered-map/v2/CHANGELOG.md | 38 + .../github.com/wk8/go-ordered-map/v2/LICENSE | 201 +++ .../github.com/wk8/go-ordered-map/v2/Makefile | 32 + .../wk8/go-ordered-map/v2/README.md | 207 +++ .../github.com/wk8/go-ordered-map/v2/json.go | 182 +++ .../wk8/go-ordered-map/v2/orderedmap.go | 373 +++++ .../github.com/wk8/go-ordered-map/v2/yaml.go | 71 + vendor/modules.txt | 26 +- 68 files changed, 6517 insertions(+), 194 deletions(-) create mode 100644 vendor/github.com/bahlo/generic-list-go/LICENSE create mode 100644 vendor/github.com/bahlo/generic-list-go/README.md create mode 100644 vendor/github.com/bahlo/generic-list-go/list.go create mode 100644 vendor/github.com/buger/jsonparser/.gitignore create mode 100644 vendor/github.com/buger/jsonparser/.travis.yml create mode 100644 vendor/github.com/buger/jsonparser/Dockerfile create mode 100644 vendor/github.com/buger/jsonparser/LICENSE create mode 100644 vendor/github.com/buger/jsonparser/Makefile create mode 100644 vendor/github.com/buger/jsonparser/README.md create mode 100644 vendor/github.com/buger/jsonparser/bytes.go create mode 100644 vendor/github.com/buger/jsonparser/bytes_safe.go create mode 100644 vendor/github.com/buger/jsonparser/bytes_unsafe.go create mode 100644 vendor/github.com/buger/jsonparser/escape.go create mode 100644 vendor/github.com/buger/jsonparser/fuzz.go create mode 100644 vendor/github.com/buger/jsonparser/oss-fuzz-build.sh create mode 100644 vendor/github.com/buger/jsonparser/parser.go create mode 100644 vendor/github.com/uptrace/bun/dialect/pgdialect/alter_table.go create mode 100644 vendor/github.com/uptrace/bun/dialect/pgdialect/inspector.go create mode 100644 vendor/github.com/uptrace/bun/migrate/auto.go create mode 100644 vendor/github.com/uptrace/bun/migrate/diff.go create mode 100644 vendor/github.com/uptrace/bun/migrate/operations.go create mode 100644 vendor/github.com/uptrace/bun/migrate/sqlschema/column.go create mode 100644 vendor/github.com/uptrace/bun/migrate/sqlschema/database.go create mode 100644 vendor/github.com/uptrace/bun/migrate/sqlschema/inspector.go create mode 100644 vendor/github.com/uptrace/bun/migrate/sqlschema/migrator.go create mode 100644 vendor/github.com/uptrace/bun/migrate/sqlschema/table.go create mode 100644 vendor/github.com/wk8/go-ordered-map/v2/.gitignore create mode 100644 vendor/github.com/wk8/go-ordered-map/v2/.golangci.yml create mode 100644 vendor/github.com/wk8/go-ordered-map/v2/CHANGELOG.md create mode 100644 vendor/github.com/wk8/go-ordered-map/v2/LICENSE create mode 100644 vendor/github.com/wk8/go-ordered-map/v2/Makefile create mode 100644 vendor/github.com/wk8/go-ordered-map/v2/README.md create mode 100644 vendor/github.com/wk8/go-ordered-map/v2/json.go create mode 100644 vendor/github.com/wk8/go-ordered-map/v2/orderedmap.go create mode 100644 vendor/github.com/wk8/go-ordered-map/v2/yaml.go diff --git a/go.mod b/go.mod index e95aecc68..d8e34f7d1 100644 --- a/go.mod +++ b/go.mod @@ -76,10 +76,10 @@ require ( github.com/tetratelabs/wazero v1.8.1 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 github.com/ulule/limiter/v3 v3.11.2 - github.com/uptrace/bun v1.2.5 - github.com/uptrace/bun/dialect/pgdialect v1.2.5 - github.com/uptrace/bun/dialect/sqlitedialect v1.2.5 - github.com/uptrace/bun/extra/bunotel v1.2.5 + github.com/uptrace/bun v1.2.6 + github.com/uptrace/bun/dialect/pgdialect v1.2.6 + github.com/uptrace/bun/dialect/sqlitedialect v1.2.6 + github.com/uptrace/bun/extra/bunotel v1.2.6 github.com/wagslane/go-password-validator v0.3.0 github.com/yuin/goldmark v1.7.8 go.opentelemetry.io/otel v1.32.0 @@ -111,7 +111,9 @@ require ( github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aymerick/douceur v0.2.0 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect @@ -224,6 +226,7 @@ require ( github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41 // indirect go.mongodb.org/mongo-driver v1.14.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect diff --git a/go.sum b/go.sum index 3ce2e8853..ce53cf54b 100644 --- a/go.sum +++ b/go.sum @@ -97,10 +97,14 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/buckket/go-blurhash v1.1.0 h1:X5M6r0LIvwdvKiUtiNcRL2YlmOfMzYobI3VCKCZc9Do= github.com/buckket/go-blurhash v1.1.0/go.mod h1:aT2iqo5W9vu9GpyoLErKfTHwgODsZp3bQfXjXJUxNb8= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= @@ -581,14 +585,14 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ulule/limiter/v3 v3.11.2 h1:P4yOrxoEMJbOTfRJR2OzjL90oflzYPPmWg+dvwN2tHA= github.com/ulule/limiter/v3 v3.11.2/go.mod h1:QG5GnFOCV+k7lrL5Y8kgEeeflPH3+Cviqlqa8SVSQxI= -github.com/uptrace/bun v1.2.5 h1:gSprL5xiBCp+tzcZHgENzJpXnmQwRM/A6s4HnBF85mc= -github.com/uptrace/bun v1.2.5/go.mod h1:vkQMS4NNs4VNZv92y53uBSHXRqYyJp4bGhMHgaNCQpY= -github.com/uptrace/bun/dialect/pgdialect v1.2.5 h1:dWLUxpjTdglzfBks2x+U2WIi+nRVjuh7Z3DLYVFswJk= -github.com/uptrace/bun/dialect/pgdialect v1.2.5/go.mod h1:stwnlE8/6x8cuQ2aXcZqwDK/d+6jxgO3iQewflJT6C4= -github.com/uptrace/bun/dialect/sqlitedialect v1.2.5 h1:liDvMaIWrN8DrHcxVbviOde/VDss9uhcqpcTSL3eJjc= -github.com/uptrace/bun/dialect/sqlitedialect v1.2.5/go.mod h1:Mw6IDL/jNUL5ozcREAezOJSZ9Jm4LJlfoaXxBEfNBlM= -github.com/uptrace/bun/extra/bunotel v1.2.5 h1:kkuuTbrG9d5leYZuSBKhq2gtq346lIrxf98Mig2y128= -github.com/uptrace/bun/extra/bunotel v1.2.5/go.mod h1:rCHLszRZwppWE9cGDodO2FCI1qCrLwDjONp38KD3bA8= +github.com/uptrace/bun v1.2.6 h1:lyGBQAhNiClchb97HA2cBnDeRxwTRLhSIgiFPXVisV8= +github.com/uptrace/bun v1.2.6/go.mod h1:xMgnVFf+/5xsrFBU34HjDJmzZnXbVuNEt/Ih56I8qBU= +github.com/uptrace/bun/dialect/pgdialect v1.2.6 h1:iNd1YLx619K+sZK+dRcWPzluurXYK1QwIkp9FEfNB/8= +github.com/uptrace/bun/dialect/pgdialect v1.2.6/go.mod h1:OL7d3qZLxKYP8kxNhMg3IheN1pDR3UScGjoUP+ivxJQ= +github.com/uptrace/bun/dialect/sqlitedialect v1.2.6 h1:p8vA39kR9Ypw0so+gUhFhd8NOufx3MzvoxJeUpwieQU= +github.com/uptrace/bun/dialect/sqlitedialect v1.2.6/go.mod h1:sdGy8eCv9WVGDrPhagE9i7FASeyj3BFkHzkRMF/qK3w= +github.com/uptrace/bun/extra/bunotel v1.2.6 h1:6m90acv9hsDuTYRo3oiKCWMatGPmi+feKAx8Y/GPj9A= +github.com/uptrace/bun/extra/bunotel v1.2.6/go.mod h1:QGqnFNJ2H88juh7DmgdPJZVN9bSTpj7UaGllSO9JDKk= github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 h1:ZjUj9BLYf9PEqBn8W/OapxhPjVRdC6CsXTdULHsyk5c= github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2/go.mod h1:O8bHQfyinKwTXKkiKNGmLQS7vRsqRxIQTFZpYpHK3IQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -603,6 +607,8 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I= github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ= +github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41 h1:rnB8ZLMeAr3VcqjfRkAm27qb8y6zFKNfuHvy1Gfe7KI= +github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41/go.mod h1:DbzwytT4g/odXquuOCqroKvtxxldI4nb3nuesHF/Exo= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= diff --git a/vendor/github.com/bahlo/generic-list-go/LICENSE b/vendor/github.com/bahlo/generic-list-go/LICENSE new file mode 100644 index 000000000..6a66aea5e --- /dev/null +++ b/vendor/github.com/bahlo/generic-list-go/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/bahlo/generic-list-go/README.md b/vendor/github.com/bahlo/generic-list-go/README.md new file mode 100644 index 000000000..68bbce9fb --- /dev/null +++ b/vendor/github.com/bahlo/generic-list-go/README.md @@ -0,0 +1,5 @@ +# generic-list-go [![CI](https://github.com/bahlo/generic-list-go/actions/workflows/ci.yml/badge.svg)](https://github.com/bahlo/generic-list-go/actions/workflows/ci.yml) + +Go [container/list](https://pkg.go.dev/container/list) but with generics. + +The code is based on `container/list` in `go1.18beta2`. diff --git a/vendor/github.com/bahlo/generic-list-go/list.go b/vendor/github.com/bahlo/generic-list-go/list.go new file mode 100644 index 000000000..a06a7c612 --- /dev/null +++ b/vendor/github.com/bahlo/generic-list-go/list.go @@ -0,0 +1,235 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package list implements a doubly linked list. +// +// To iterate over a list (where l is a *List): +// for e := l.Front(); e != nil; e = e.Next() { +// // do something with e.Value +// } +// +package list + +// Element is an element of a linked list. +type Element[T any] struct { + // Next and previous pointers in the doubly-linked list of elements. + // To simplify the implementation, internally a list l is implemented + // as a ring, such that &l.root is both the next element of the last + // list element (l.Back()) and the previous element of the first list + // element (l.Front()). + next, prev *Element[T] + + // The list to which this element belongs. + list *List[T] + + // The value stored with this element. + Value T +} + +// Next returns the next list element or nil. +func (e *Element[T]) Next() *Element[T] { + if p := e.next; e.list != nil && p != &e.list.root { + return p + } + return nil +} + +// Prev returns the previous list element or nil. +func (e *Element[T]) Prev() *Element[T] { + if p := e.prev; e.list != nil && p != &e.list.root { + return p + } + return nil +} + +// List represents a doubly linked list. +// The zero value for List is an empty list ready to use. +type List[T any] struct { + root Element[T] // sentinel list element, only &root, root.prev, and root.next are used + len int // current list length excluding (this) sentinel element +} + +// Init initializes or clears list l. +func (l *List[T]) Init() *List[T] { + l.root.next = &l.root + l.root.prev = &l.root + l.len = 0 + return l +} + +// New returns an initialized list. +func New[T any]() *List[T] { return new(List[T]).Init() } + +// Len returns the number of elements of list l. +// The complexity is O(1). +func (l *List[T]) Len() int { return l.len } + +// Front returns the first element of list l or nil if the list is empty. +func (l *List[T]) Front() *Element[T] { + if l.len == 0 { + return nil + } + return l.root.next +} + +// Back returns the last element of list l or nil if the list is empty. +func (l *List[T]) Back() *Element[T] { + if l.len == 0 { + return nil + } + return l.root.prev +} + +// lazyInit lazily initializes a zero List value. +func (l *List[T]) lazyInit() { + if l.root.next == nil { + l.Init() + } +} + +// insert inserts e after at, increments l.len, and returns e. +func (l *List[T]) insert(e, at *Element[T]) *Element[T] { + e.prev = at + e.next = at.next + e.prev.next = e + e.next.prev = e + e.list = l + l.len++ + return e +} + +// insertValue is a convenience wrapper for insert(&Element{Value: v}, at). +func (l *List[T]) insertValue(v T, at *Element[T]) *Element[T] { + return l.insert(&Element[T]{Value: v}, at) +} + +// remove removes e from its list, decrements l.len +func (l *List[T]) remove(e *Element[T]) { + e.prev.next = e.next + e.next.prev = e.prev + e.next = nil // avoid memory leaks + e.prev = nil // avoid memory leaks + e.list = nil + l.len-- +} + +// move moves e to next to at. +func (l *List[T]) move(e, at *Element[T]) { + if e == at { + return + } + e.prev.next = e.next + e.next.prev = e.prev + + e.prev = at + e.next = at.next + e.prev.next = e + e.next.prev = e +} + +// Remove removes e from l if e is an element of list l. +// It returns the element value e.Value. +// The element must not be nil. +func (l *List[T]) Remove(e *Element[T]) T { + if e.list == l { + // if e.list == l, l must have been initialized when e was inserted + // in l or l == nil (e is a zero Element) and l.remove will crash + l.remove(e) + } + return e.Value +} + +// PushFront inserts a new element e with value v at the front of list l and returns e. +func (l *List[T]) PushFront(v T) *Element[T] { + l.lazyInit() + return l.insertValue(v, &l.root) +} + +// PushBack inserts a new element e with value v at the back of list l and returns e. +func (l *List[T]) PushBack(v T) *Element[T] { + l.lazyInit() + return l.insertValue(v, l.root.prev) +} + +// InsertBefore inserts a new element e with value v immediately before mark and returns e. +// If mark is not an element of l, the list is not modified. +// The mark must not be nil. +func (l *List[T]) InsertBefore(v T, mark *Element[T]) *Element[T] { + if mark.list != l { + return nil + } + // see comment in List.Remove about initialization of l + return l.insertValue(v, mark.prev) +} + +// InsertAfter inserts a new element e with value v immediately after mark and returns e. +// If mark is not an element of l, the list is not modified. +// The mark must not be nil. +func (l *List[T]) InsertAfter(v T, mark *Element[T]) *Element[T] { + if mark.list != l { + return nil + } + // see comment in List.Remove about initialization of l + return l.insertValue(v, mark) +} + +// MoveToFront moves element e to the front of list l. +// If e is not an element of l, the list is not modified. +// The element must not be nil. +func (l *List[T]) MoveToFront(e *Element[T]) { + if e.list != l || l.root.next == e { + return + } + // see comment in List.Remove about initialization of l + l.move(e, &l.root) +} + +// MoveToBack moves element e to the back of list l. +// If e is not an element of l, the list is not modified. +// The element must not be nil. +func (l *List[T]) MoveToBack(e *Element[T]) { + if e.list != l || l.root.prev == e { + return + } + // see comment in List.Remove about initialization of l + l.move(e, l.root.prev) +} + +// MoveBefore moves element e to its new position before mark. +// If e or mark is not an element of l, or e == mark, the list is not modified. +// The element and mark must not be nil. +func (l *List[T]) MoveBefore(e, mark *Element[T]) { + if e.list != l || e == mark || mark.list != l { + return + } + l.move(e, mark.prev) +} + +// MoveAfter moves element e to its new position after mark. +// If e or mark is not an element of l, or e == mark, the list is not modified. +// The element and mark must not be nil. +func (l *List[T]) MoveAfter(e, mark *Element[T]) { + if e.list != l || e == mark || mark.list != l { + return + } + l.move(e, mark) +} + +// PushBackList inserts a copy of another list at the back of list l. +// The lists l and other may be the same. They must not be nil. +func (l *List[T]) PushBackList(other *List[T]) { + l.lazyInit() + for i, e := other.Len(), other.Front(); i > 0; i, e = i-1, e.Next() { + l.insertValue(e.Value, l.root.prev) + } +} + +// PushFrontList inserts a copy of another list at the front of list l. +// The lists l and other may be the same. They must not be nil. +func (l *List[T]) PushFrontList(other *List[T]) { + l.lazyInit() + for i, e := other.Len(), other.Back(); i > 0; i, e = i-1, e.Prev() { + l.insertValue(e.Value, &l.root) + } +} diff --git a/vendor/github.com/buger/jsonparser/.gitignore b/vendor/github.com/buger/jsonparser/.gitignore new file mode 100644 index 000000000..5598d8a56 --- /dev/null +++ b/vendor/github.com/buger/jsonparser/.gitignore @@ -0,0 +1,12 @@ + +*.test + +*.out + +*.mprof + +.idea + +vendor/github.com/buger/goterm/ +prof.cpu +prof.mem diff --git a/vendor/github.com/buger/jsonparser/.travis.yml b/vendor/github.com/buger/jsonparser/.travis.yml new file mode 100644 index 000000000..dbfb7cf98 --- /dev/null +++ b/vendor/github.com/buger/jsonparser/.travis.yml @@ -0,0 +1,11 @@ +language: go +arch: + - amd64 + - ppc64le +go: + - 1.7.x + - 1.8.x + - 1.9.x + - 1.10.x + - 1.11.x +script: go test -v ./. diff --git a/vendor/github.com/buger/jsonparser/Dockerfile b/vendor/github.com/buger/jsonparser/Dockerfile new file mode 100644 index 000000000..37fc9fd0b --- /dev/null +++ b/vendor/github.com/buger/jsonparser/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:1.6 + +RUN go get github.com/Jeffail/gabs +RUN go get github.com/bitly/go-simplejson +RUN go get github.com/pquerna/ffjson +RUN go get github.com/antonholmquist/jason +RUN go get github.com/mreiferson/go-ujson +RUN go get -tags=unsafe -u github.com/ugorji/go/codec +RUN go get github.com/mailru/easyjson + +WORKDIR /go/src/github.com/buger/jsonparser +ADD . /go/src/github.com/buger/jsonparser \ No newline at end of file diff --git a/vendor/github.com/buger/jsonparser/LICENSE b/vendor/github.com/buger/jsonparser/LICENSE new file mode 100644 index 000000000..ac25aeb7d --- /dev/null +++ b/vendor/github.com/buger/jsonparser/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Leonid Bugaev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/buger/jsonparser/Makefile b/vendor/github.com/buger/jsonparser/Makefile new file mode 100644 index 000000000..e843368cf --- /dev/null +++ b/vendor/github.com/buger/jsonparser/Makefile @@ -0,0 +1,36 @@ +SOURCE = parser.go +CONTAINER = jsonparser +SOURCE_PATH = /go/src/github.com/buger/jsonparser +BENCHMARK = JsonParser +BENCHTIME = 5s +TEST = . +DRUN = docker run -v `pwd`:$(SOURCE_PATH) -i -t $(CONTAINER) + +build: + docker build -t $(CONTAINER) . + +race: + $(DRUN) --env GORACE="halt_on_error=1" go test ./. $(ARGS) -v -race -timeout 15s + +bench: + $(DRUN) go test $(LDFLAGS) -test.benchmem -bench $(BENCHMARK) ./benchmark/ $(ARGS) -benchtime $(BENCHTIME) -v + +bench_local: + $(DRUN) go test $(LDFLAGS) -test.benchmem -bench . $(ARGS) -benchtime $(BENCHTIME) -v + +profile: + $(DRUN) go test $(LDFLAGS) -test.benchmem -bench $(BENCHMARK) ./benchmark/ $(ARGS) -memprofile mem.mprof -v + $(DRUN) go test $(LDFLAGS) -test.benchmem -bench $(BENCHMARK) ./benchmark/ $(ARGS) -cpuprofile cpu.out -v + $(DRUN) go test $(LDFLAGS) -test.benchmem -bench $(BENCHMARK) ./benchmark/ $(ARGS) -c + +test: + $(DRUN) go test $(LDFLAGS) ./ -run $(TEST) -timeout 10s $(ARGS) -v + +fmt: + $(DRUN) go fmt ./... + +vet: + $(DRUN) go vet ./. + +bash: + $(DRUN) /bin/bash \ No newline at end of file diff --git a/vendor/github.com/buger/jsonparser/README.md b/vendor/github.com/buger/jsonparser/README.md new file mode 100644 index 000000000..d7e0ec397 --- /dev/null +++ b/vendor/github.com/buger/jsonparser/README.md @@ -0,0 +1,365 @@ +[![Go Report Card](https://goreportcard.com/badge/github.com/buger/jsonparser)](https://goreportcard.com/report/github.com/buger/jsonparser) ![License](https://img.shields.io/dub/l/vibe-d.svg) +# Alternative JSON parser for Go (10x times faster standard library) + +It does not require you to know the structure of the payload (eg. create structs), and allows accessing fields by providing the path to them. It is up to **10 times faster** than standard `encoding/json` package (depending on payload size and usage), **allocates no memory**. See benchmarks below. + +## Rationale +Originally I made this for a project that relies on a lot of 3rd party APIs that can be unpredictable and complex. +I love simplicity and prefer to avoid external dependecies. `encoding/json` requires you to know exactly your data structures, or if you prefer to use `map[string]interface{}` instead, it will be very slow and hard to manage. +I investigated what's on the market and found that most libraries are just wrappers around `encoding/json`, there is few options with own parsers (`ffjson`, `easyjson`), but they still requires you to create data structures. + + +Goal of this project is to push JSON parser to the performance limits and not sacrifice with compliance and developer user experience. + +## Example +For the given JSON our goal is to extract the user's full name, number of github followers and avatar. + +```go +import "github.com/buger/jsonparser" + +... + +data := []byte(`{ + "person": { + "name": { + "first": "Leonid", + "last": "Bugaev", + "fullName": "Leonid Bugaev" + }, + "github": { + "handle": "buger", + "followers": 109 + }, + "avatars": [ + { "url": "https://avatars1.githubusercontent.com/u/14009?v=3&s=460", "type": "thumbnail" } + ] + }, + "company": { + "name": "Acme" + } +}`) + +// You can specify key path by providing arguments to Get function +jsonparser.Get(data, "person", "name", "fullName") + +// There is `GetInt` and `GetBoolean` helpers if you exactly know key data type +jsonparser.GetInt(data, "person", "github", "followers") + +// When you try to get object, it will return you []byte slice pointer to data containing it +// In `company` it will be `{"name": "Acme"}` +jsonparser.Get(data, "company") + +// If the key doesn't exist it will throw an error +var size int64 +if value, err := jsonparser.GetInt(data, "company", "size"); err == nil { + size = value +} + +// You can use `ArrayEach` helper to iterate items [item1, item2 .... itemN] +jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) { + fmt.Println(jsonparser.Get(value, "url")) +}, "person", "avatars") + +// Or use can access fields by index! +jsonparser.GetString(data, "person", "avatars", "[0]", "url") + +// You can use `ObjectEach` helper to iterate objects { "key1":object1, "key2":object2, .... "keyN":objectN } +jsonparser.ObjectEach(data, func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) error { + fmt.Printf("Key: '%s'\n Value: '%s'\n Type: %s\n", string(key), string(value), dataType) + return nil +}, "person", "name") + +// The most efficient way to extract multiple keys is `EachKey` + +paths := [][]string{ + []string{"person", "name", "fullName"}, + []string{"person", "avatars", "[0]", "url"}, + []string{"company", "url"}, +} +jsonparser.EachKey(data, func(idx int, value []byte, vt jsonparser.ValueType, err error){ + switch idx { + case 0: // []string{"person", "name", "fullName"} + ... + case 1: // []string{"person", "avatars", "[0]", "url"} + ... + case 2: // []string{"company", "url"}, + ... + } +}, paths...) + +// For more information see docs below +``` + +## Need to speedup your app? + +I'm available for consulting and can help you push your app performance to the limits. Ping me at: leonsbox@gmail.com. + +## Reference + +Library API is really simple. You just need the `Get` method to perform any operation. The rest is just helpers around it. + +You also can view API at [godoc.org](https://godoc.org/github.com/buger/jsonparser) + + +### **`Get`** +```go +func Get(data []byte, keys ...string) (value []byte, dataType jsonparser.ValueType, offset int, err error) +``` +Receives data structure, and key path to extract value from. + +Returns: +* `value` - Pointer to original data structure containing key value, or just empty slice if nothing found or error +* `dataType` - Can be: `NotExist`, `String`, `Number`, `Object`, `Array`, `Boolean` or `Null` +* `offset` - Offset from provided data structure where key value ends. Used mostly internally, for example for `ArrayEach` helper. +* `err` - If the key is not found or any other parsing issue, it should return error. If key not found it also sets `dataType` to `NotExist` + +Accepts multiple keys to specify path to JSON value (in case of quering nested structures). +If no keys are provided it will try to extract the closest JSON value (simple ones or object/array), useful for reading streams or arrays, see `ArrayEach` implementation. + +Note that keys can be an array indexes: `jsonparser.GetInt("person", "avatars", "[0]", "url")`, pretty cool, yeah? + +### **`GetString`** +```go +func GetString(data []byte, keys ...string) (val string, err error) +``` +Returns strings properly handing escaped and unicode characters. Note that this will cause additional memory allocations. + +### **`GetUnsafeString`** +If you need string in your app, and ready to sacrifice with support of escaped symbols in favor of speed. It returns string mapped to existing byte slice memory, without any allocations: +```go +s, _, := jsonparser.GetUnsafeString(data, "person", "name", "title") +switch s { + case 'CEO': + ... + case 'Engineer' + ... + ... +} +``` +Note that `unsafe` here means that your string will exist until GC will free underlying byte slice, for most of cases it means that you can use this string only in current context, and should not pass it anywhere externally: through channels or any other way. + + +### **`GetBoolean`**, **`GetInt`** and **`GetFloat`** +```go +func GetBoolean(data []byte, keys ...string) (val bool, err error) + +func GetFloat(data []byte, keys ...string) (val float64, err error) + +func GetInt(data []byte, keys ...string) (val int64, err error) +``` +If you know the key type, you can use the helpers above. +If key data type do not match, it will return error. + +### **`ArrayEach`** +```go +func ArrayEach(data []byte, cb func(value []byte, dataType jsonparser.ValueType, offset int, err error), keys ...string) +``` +Needed for iterating arrays, accepts a callback function with the same return arguments as `Get`. + +### **`ObjectEach`** +```go +func ObjectEach(data []byte, callback func(key []byte, value []byte, dataType ValueType, offset int) error, keys ...string) (err error) +``` +Needed for iterating object, accepts a callback function. Example: +```go +var handler func([]byte, []byte, jsonparser.ValueType, int) error +handler = func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) error { + //do stuff here +} +jsonparser.ObjectEach(myJson, handler) +``` + + +### **`EachKey`** +```go +func EachKey(data []byte, cb func(idx int, value []byte, dataType jsonparser.ValueType, err error), paths ...[]string) +``` +When you need to read multiple keys, and you do not afraid of low-level API `EachKey` is your friend. It read payload only single time, and calls callback function once path is found. For example when you call multiple times `Get`, it has to process payload multiple times, each time you call it. Depending on payload `EachKey` can be multiple times faster than `Get`. Path can use nested keys as well! + +```go +paths := [][]string{ + []string{"uuid"}, + []string{"tz"}, + []string{"ua"}, + []string{"st"}, +} +var data SmallPayload + +jsonparser.EachKey(smallFixture, func(idx int, value []byte, vt jsonparser.ValueType, err error){ + switch idx { + case 0: + data.Uuid, _ = value + case 1: + v, _ := jsonparser.ParseInt(value) + data.Tz = int(v) + case 2: + data.Ua, _ = value + case 3: + v, _ := jsonparser.ParseInt(value) + data.St = int(v) + } +}, paths...) +``` + +### **`Set`** +```go +func Set(data []byte, setValue []byte, keys ...string) (value []byte, err error) +``` +Receives existing data structure, key path to set, and value to set at that key. *This functionality is experimental.* + +Returns: +* `value` - Pointer to original data structure with updated or added key value. +* `err` - If any parsing issue, it should return error. + +Accepts multiple keys to specify path to JSON value (in case of updating or creating nested structures). + +Note that keys can be an array indexes: `jsonparser.Set(data, []byte("http://github.com"), "person", "avatars", "[0]", "url")` + +### **`Delete`** +```go +func Delete(data []byte, keys ...string) value []byte +``` +Receives existing data structure, and key path to delete. *This functionality is experimental.* + +Returns: +* `value` - Pointer to original data structure with key path deleted if it can be found. If there is no key path, then the whole data structure is deleted. + +Accepts multiple keys to specify path to JSON value (in case of updating or creating nested structures). + +Note that keys can be an array indexes: `jsonparser.Delete(data, "person", "avatars", "[0]", "url")` + + +## What makes it so fast? +* It does not rely on `encoding/json`, `reflection` or `interface{}`, the only real package dependency is `bytes`. +* Operates with JSON payload on byte level, providing you pointers to the original data structure: no memory allocation. +* No automatic type conversions, by default everything is a []byte, but it provides you value type, so you can convert by yourself (there is few helpers included). +* Does not parse full record, only keys you specified + + +## Benchmarks + +There are 3 benchmark types, trying to simulate real-life usage for small, medium and large JSON payloads. +For each metric, the lower value is better. Time/op is in nanoseconds. Values better than standard encoding/json marked as bold text. +Benchmarks run on standard Linode 1024 box. + +Compared libraries: +* https://golang.org/pkg/encoding/json +* https://github.com/Jeffail/gabs +* https://github.com/a8m/djson +* https://github.com/bitly/go-simplejson +* https://github.com/antonholmquist/jason +* https://github.com/mreiferson/go-ujson +* https://github.com/ugorji/go/codec +* https://github.com/pquerna/ffjson +* https://github.com/mailru/easyjson +* https://github.com/buger/jsonparser + +#### TLDR +If you want to skip next sections we have 2 winner: `jsonparser` and `easyjson`. +`jsonparser` is up to 10 times faster than standard `encoding/json` package (depending on payload size and usage), and almost infinitely (literally) better in memory consumption because it operates with data on byte level, and provide direct slice pointers. +`easyjson` wins in CPU in medium tests and frankly i'm impressed with this package: it is remarkable results considering that it is almost drop-in replacement for `encoding/json` (require some code generation). + +It's hard to fully compare `jsonparser` and `easyjson` (or `ffson`), they a true parsers and fully process record, unlike `jsonparser` which parse only keys you specified. + +If you searching for replacement of `encoding/json` while keeping structs, `easyjson` is an amazing choice. If you want to process dynamic JSON, have memory constrains, or more control over your data you should try `jsonparser`. + +`jsonparser` performance heavily depends on usage, and it works best when you do not need to process full record, only some keys. The more calls you need to make, the slower it will be, in contrast `easyjson` (or `ffjson`, `encoding/json`) parser record only 1 time, and then you can make as many calls as you want. + +With great power comes great responsibility! :) + + +#### Small payload + +Each test processes 190 bytes of http log as a JSON record. +It should read multiple fields. +https://github.com/buger/jsonparser/blob/master/benchmark/benchmark_small_payload_test.go + +Library | time/op | bytes/op | allocs/op + ------ | ------- | -------- | ------- +encoding/json struct | 7879 | 880 | 18 +encoding/json interface{} | 8946 | 1521 | 38 +Jeffail/gabs | 10053 | 1649 | 46 +bitly/go-simplejson | 10128 | 2241 | 36 +antonholmquist/jason | 27152 | 7237 | 101 +github.com/ugorji/go/codec | 8806 | 2176 | 31 +mreiferson/go-ujson | **7008** | **1409** | 37 +a8m/djson | 3862 | 1249 | 30 +pquerna/ffjson | **3769** | **624** | **15** +mailru/easyjson | **2002** | **192** | **9** +buger/jsonparser | **1367** | **0** | **0** +buger/jsonparser (EachKey API) | **809** | **0** | **0** + +Winners are ffjson, easyjson and jsonparser, where jsonparser is up to 9.8x faster than encoding/json and 4.6x faster than ffjson, and slightly faster than easyjson. +If you look at memory allocation, jsonparser has no rivals, as it makes no data copy and operates with raw []byte structures and pointers to it. + +#### Medium payload + +Each test processes a 2.4kb JSON record (based on Clearbit API). +It should read multiple nested fields and 1 array. + +https://github.com/buger/jsonparser/blob/master/benchmark/benchmark_medium_payload_test.go + +| Library | time/op | bytes/op | allocs/op | +| ------- | ------- | -------- | --------- | +| encoding/json struct | 57749 | 1336 | 29 | +| encoding/json interface{} | 79297 | 10627 | 215 | +| Jeffail/gabs | 83807 | 11202 | 235 | +| bitly/go-simplejson | 88187 | 17187 | 220 | +| antonholmquist/jason | 94099 | 19013 | 247 | +| github.com/ugorji/go/codec | 114719 | 6712 | 152 | +| mreiferson/go-ujson | **56972** | 11547 | 270 | +| a8m/djson | 28525 | 10196 | 198 | +| pquerna/ffjson | **20298** | **856** | **20** | +| mailru/easyjson | **10512** | **336** | **12** | +| buger/jsonparser | **15955** | **0** | **0** | +| buger/jsonparser (EachKey API) | **8916** | **0** | **0** | + +The difference between ffjson and jsonparser in CPU usage is smaller, while the memory consumption difference is growing. On the other hand `easyjson` shows remarkable performance for medium payload. + +`gabs`, `go-simplejson` and `jason` are based on encoding/json and map[string]interface{} and actually only helpers for unstructured JSON, their performance correlate with `encoding/json interface{}`, and they will skip next round. +`go-ujson` while have its own parser, shows same performance as `encoding/json`, also skips next round. Same situation with `ugorji/go/codec`, but it showed unexpectedly bad performance for complex payloads. + + +#### Large payload + +Each test processes a 24kb JSON record (based on Discourse API) +It should read 2 arrays, and for each item in array get a few fields. +Basically it means processing a full JSON file. + +https://github.com/buger/jsonparser/blob/master/benchmark/benchmark_large_payload_test.go + +| Library | time/op | bytes/op | allocs/op | +| --- | --- | --- | --- | +| encoding/json struct | 748336 | 8272 | 307 | +| encoding/json interface{} | 1224271 | 215425 | 3395 | +| a8m/djson | 510082 | 213682 | 2845 | +| pquerna/ffjson | **312271** | **7792** | **298** | +| mailru/easyjson | **154186** | **6992** | **288** | +| buger/jsonparser | **85308** | **0** | **0** | + +`jsonparser` now is a winner, but do not forget that it is way more lightweight parser than `ffson` or `easyjson`, and they have to parser all the data, while `jsonparser` parse only what you need. All `ffjson`, `easysjon` and `jsonparser` have their own parsing code, and does not depend on `encoding/json` or `interface{}`, thats one of the reasons why they are so fast. `easyjson` also use a bit of `unsafe` package to reduce memory consuption (in theory it can lead to some unexpected GC issue, but i did not tested enough) + +Also last benchmark did not included `EachKey` test, because in this particular case we need to read lot of Array values, and using `ArrayEach` is more efficient. + +## Questions and support + +All bug-reports and suggestions should go though Github Issues. + +## Contributing + +1. Fork it +2. Create your feature branch (git checkout -b my-new-feature) +3. Commit your changes (git commit -am 'Added some feature') +4. Push to the branch (git push origin my-new-feature) +5. Create new Pull Request + +## Development + +All my development happens using Docker, and repo include some Make tasks to simplify development. + +* `make build` - builds docker image, usually can be called only once +* `make test` - run tests +* `make fmt` - run go fmt +* `make bench` - run benchmarks (if you need to run only single benchmark modify `BENCHMARK` variable in make file) +* `make profile` - runs benchmark and generate 3 files- `cpu.out`, `mem.mprof` and `benchmark.test` binary, which can be used for `go tool pprof` +* `make bash` - enter container (i use it for running `go tool pprof` above) diff --git a/vendor/github.com/buger/jsonparser/bytes.go b/vendor/github.com/buger/jsonparser/bytes.go new file mode 100644 index 000000000..0bb0ff395 --- /dev/null +++ b/vendor/github.com/buger/jsonparser/bytes.go @@ -0,0 +1,47 @@ +package jsonparser + +import ( + bio "bytes" +) + +// minInt64 '-9223372036854775808' is the smallest representable number in int64 +const minInt64 = `9223372036854775808` + +// About 2x faster then strconv.ParseInt because it only supports base 10, which is enough for JSON +func parseInt(bytes []byte) (v int64, ok bool, overflow bool) { + if len(bytes) == 0 { + return 0, false, false + } + + var neg bool = false + if bytes[0] == '-' { + neg = true + bytes = bytes[1:] + } + + var b int64 = 0 + for _, c := range bytes { + if c >= '0' && c <= '9' { + b = (10 * v) + int64(c-'0') + } else { + return 0, false, false + } + if overflow = (b < v); overflow { + break + } + v = b + } + + if overflow { + if neg && bio.Equal(bytes, []byte(minInt64)) { + return b, true, false + } + return 0, false, true + } + + if neg { + return -v, true, false + } else { + return v, true, false + } +} diff --git a/vendor/github.com/buger/jsonparser/bytes_safe.go b/vendor/github.com/buger/jsonparser/bytes_safe.go new file mode 100644 index 000000000..ff16a4a19 --- /dev/null +++ b/vendor/github.com/buger/jsonparser/bytes_safe.go @@ -0,0 +1,25 @@ +// +build appengine appenginevm + +package jsonparser + +import ( + "strconv" +) + +// See fastbytes_unsafe.go for explanation on why *[]byte is used (signatures must be consistent with those in that file) + +func equalStr(b *[]byte, s string) bool { + return string(*b) == s +} + +func parseFloat(b *[]byte) (float64, error) { + return strconv.ParseFloat(string(*b), 64) +} + +func bytesToString(b *[]byte) string { + return string(*b) +} + +func StringToBytes(s string) []byte { + return []byte(s) +} diff --git a/vendor/github.com/buger/jsonparser/bytes_unsafe.go b/vendor/github.com/buger/jsonparser/bytes_unsafe.go new file mode 100644 index 000000000..589fea87e --- /dev/null +++ b/vendor/github.com/buger/jsonparser/bytes_unsafe.go @@ -0,0 +1,44 @@ +// +build !appengine,!appenginevm + +package jsonparser + +import ( + "reflect" + "strconv" + "unsafe" + "runtime" +) + +// +// The reason for using *[]byte rather than []byte in parameters is an optimization. As of Go 1.6, +// the compiler cannot perfectly inline the function when using a non-pointer slice. That is, +// the non-pointer []byte parameter version is slower than if its function body is manually +// inlined, whereas the pointer []byte version is equally fast to the manually inlined +// version. Instruction count in assembly taken from "go tool compile" confirms this difference. +// +// TODO: Remove hack after Go 1.7 release +// +func equalStr(b *[]byte, s string) bool { + return *(*string)(unsafe.Pointer(b)) == s +} + +func parseFloat(b *[]byte) (float64, error) { + return strconv.ParseFloat(*(*string)(unsafe.Pointer(b)), 64) +} + +// A hack until issue golang/go#2632 is fixed. +// See: https://github.com/golang/go/issues/2632 +func bytesToString(b *[]byte) string { + return *(*string)(unsafe.Pointer(b)) +} + +func StringToBytes(s string) []byte { + b := make([]byte, 0, 0) + bh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) + sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) + bh.Data = sh.Data + bh.Cap = sh.Len + bh.Len = sh.Len + runtime.KeepAlive(s) + return b +} diff --git a/vendor/github.com/buger/jsonparser/escape.go b/vendor/github.com/buger/jsonparser/escape.go new file mode 100644 index 000000000..49669b942 --- /dev/null +++ b/vendor/github.com/buger/jsonparser/escape.go @@ -0,0 +1,173 @@ +package jsonparser + +import ( + "bytes" + "unicode/utf8" +) + +// JSON Unicode stuff: see https://tools.ietf.org/html/rfc7159#section-7 + +const supplementalPlanesOffset = 0x10000 +const highSurrogateOffset = 0xD800 +const lowSurrogateOffset = 0xDC00 + +const basicMultilingualPlaneReservedOffset = 0xDFFF +const basicMultilingualPlaneOffset = 0xFFFF + +func combineUTF16Surrogates(high, low rune) rune { + return supplementalPlanesOffset + (high-highSurrogateOffset)<<10 + (low - lowSurrogateOffset) +} + +const badHex = -1 + +func h2I(c byte) int { + switch { + case c >= '0' && c <= '9': + return int(c - '0') + case c >= 'A' && c <= 'F': + return int(c - 'A' + 10) + case c >= 'a' && c <= 'f': + return int(c - 'a' + 10) + } + return badHex +} + +// decodeSingleUnicodeEscape decodes a single \uXXXX escape sequence. The prefix \u is assumed to be present and +// is not checked. +// In JSON, these escapes can either come alone or as part of "UTF16 surrogate pairs" that must be handled together. +// This function only handles one; decodeUnicodeEscape handles this more complex case. +func decodeSingleUnicodeEscape(in []byte) (rune, bool) { + // We need at least 6 characters total + if len(in) < 6 { + return utf8.RuneError, false + } + + // Convert hex to decimal + h1, h2, h3, h4 := h2I(in[2]), h2I(in[3]), h2I(in[4]), h2I(in[5]) + if h1 == badHex || h2 == badHex || h3 == badHex || h4 == badHex { + return utf8.RuneError, false + } + + // Compose the hex digits + return rune(h1<<12 + h2<<8 + h3<<4 + h4), true +} + +// isUTF16EncodedRune checks if a rune is in the range for non-BMP characters, +// which is used to describe UTF16 chars. +// Source: https://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane +func isUTF16EncodedRune(r rune) bool { + return highSurrogateOffset <= r && r <= basicMultilingualPlaneReservedOffset +} + +func decodeUnicodeEscape(in []byte) (rune, int) { + if r, ok := decodeSingleUnicodeEscape(in); !ok { + // Invalid Unicode escape + return utf8.RuneError, -1 + } else if r <= basicMultilingualPlaneOffset && !isUTF16EncodedRune(r) { + // Valid Unicode escape in Basic Multilingual Plane + return r, 6 + } else if r2, ok := decodeSingleUnicodeEscape(in[6:]); !ok { // Note: previous decodeSingleUnicodeEscape success guarantees at least 6 bytes remain + // UTF16 "high surrogate" without manditory valid following Unicode escape for the "low surrogate" + return utf8.RuneError, -1 + } else if r2 < lowSurrogateOffset { + // Invalid UTF16 "low surrogate" + return utf8.RuneError, -1 + } else { + // Valid UTF16 surrogate pair + return combineUTF16Surrogates(r, r2), 12 + } +} + +// backslashCharEscapeTable: when '\X' is found for some byte X, it is to be replaced with backslashCharEscapeTable[X] +var backslashCharEscapeTable = [...]byte{ + '"': '"', + '\\': '\\', + '/': '/', + 'b': '\b', + 'f': '\f', + 'n': '\n', + 'r': '\r', + 't': '\t', +} + +// unescapeToUTF8 unescapes the single escape sequence starting at 'in' into 'out' and returns +// how many characters were consumed from 'in' and emitted into 'out'. +// If a valid escape sequence does not appear as a prefix of 'in', (-1, -1) to signal the error. +func unescapeToUTF8(in, out []byte) (inLen int, outLen int) { + if len(in) < 2 || in[0] != '\\' { + // Invalid escape due to insufficient characters for any escape or no initial backslash + return -1, -1 + } + + // https://tools.ietf.org/html/rfc7159#section-7 + switch e := in[1]; e { + case '"', '\\', '/', 'b', 'f', 'n', 'r', 't': + // Valid basic 2-character escapes (use lookup table) + out[0] = backslashCharEscapeTable[e] + return 2, 1 + case 'u': + // Unicode escape + if r, inLen := decodeUnicodeEscape(in); inLen == -1 { + // Invalid Unicode escape + return -1, -1 + } else { + // Valid Unicode escape; re-encode as UTF8 + outLen := utf8.EncodeRune(out, r) + return inLen, outLen + } + } + + return -1, -1 +} + +// unescape unescapes the string contained in 'in' and returns it as a slice. +// If 'in' contains no escaped characters: +// Returns 'in'. +// Else, if 'out' is of sufficient capacity (guaranteed if cap(out) >= len(in)): +// 'out' is used to build the unescaped string and is returned with no extra allocation +// Else: +// A new slice is allocated and returned. +func Unescape(in, out []byte) ([]byte, error) { + firstBackslash := bytes.IndexByte(in, '\\') + if firstBackslash == -1 { + return in, nil + } + + // Get a buffer of sufficient size (allocate if needed) + if cap(out) < len(in) { + out = make([]byte, len(in)) + } else { + out = out[0:len(in)] + } + + // Copy the first sequence of unescaped bytes to the output and obtain a buffer pointer (subslice) + copy(out, in[:firstBackslash]) + in = in[firstBackslash:] + buf := out[firstBackslash:] + + for len(in) > 0 { + // Unescape the next escaped character + inLen, bufLen := unescapeToUTF8(in, buf) + if inLen == -1 { + return nil, MalformedStringEscapeError + } + + in = in[inLen:] + buf = buf[bufLen:] + + // Copy everything up until the next backslash + nextBackslash := bytes.IndexByte(in, '\\') + if nextBackslash == -1 { + copy(buf, in) + buf = buf[len(in):] + break + } else { + copy(buf, in[:nextBackslash]) + buf = buf[nextBackslash:] + in = in[nextBackslash:] + } + } + + // Trim the out buffer to the amount that was actually emitted + return out[:len(out)-len(buf)], nil +} diff --git a/vendor/github.com/buger/jsonparser/fuzz.go b/vendor/github.com/buger/jsonparser/fuzz.go new file mode 100644 index 000000000..854bd11b2 --- /dev/null +++ b/vendor/github.com/buger/jsonparser/fuzz.go @@ -0,0 +1,117 @@ +package jsonparser + +func FuzzParseString(data []byte) int { + r, err := ParseString(data) + if err != nil || r == "" { + return 0 + } + return 1 +} + +func FuzzEachKey(data []byte) int { + paths := [][]string{ + {"name"}, + {"order"}, + {"nested", "a"}, + {"nested", "b"}, + {"nested2", "a"}, + {"nested", "nested3", "b"}, + {"arr", "[1]", "b"}, + {"arrInt", "[3]"}, + {"arrInt", "[5]"}, + {"nested"}, + {"arr", "["}, + {"a\n", "b\n"}, + } + EachKey(data, func(idx int, value []byte, vt ValueType, err error) {}, paths...) + return 1 +} + +func FuzzDelete(data []byte) int { + Delete(data, "test") + return 1 +} + +func FuzzSet(data []byte) int { + _, err := Set(data, []byte(`"new value"`), "test") + if err != nil { + return 0 + } + return 1 +} + +func FuzzObjectEach(data []byte) int { + _ = ObjectEach(data, func(key, value []byte, valueType ValueType, off int) error { + return nil + }) + return 1 +} + +func FuzzParseFloat(data []byte) int { + _, err := ParseFloat(data) + if err != nil { + return 0 + } + return 1 +} + +func FuzzParseInt(data []byte) int { + _, err := ParseInt(data) + if err != nil { + return 0 + } + return 1 +} + +func FuzzParseBool(data []byte) int { + _, err := ParseBoolean(data) + if err != nil { + return 0 + } + return 1 +} + +func FuzzTokenStart(data []byte) int { + _ = tokenStart(data) + return 1 +} + +func FuzzGetString(data []byte) int { + _, err := GetString(data, "test") + if err != nil { + return 0 + } + return 1 +} + +func FuzzGetFloat(data []byte) int { + _, err := GetFloat(data, "test") + if err != nil { + return 0 + } + return 1 +} + +func FuzzGetInt(data []byte) int { + _, err := GetInt(data, "test") + if err != nil { + return 0 + } + return 1 +} + +func FuzzGetBoolean(data []byte) int { + _, err := GetBoolean(data, "test") + if err != nil { + return 0 + } + return 1 +} + +func FuzzGetUnsafeString(data []byte) int { + _, err := GetUnsafeString(data, "test") + if err != nil { + return 0 + } + return 1 +} diff --git a/vendor/github.com/buger/jsonparser/oss-fuzz-build.sh b/vendor/github.com/buger/jsonparser/oss-fuzz-build.sh new file mode 100644 index 000000000..c573b0e2d --- /dev/null +++ b/vendor/github.com/buger/jsonparser/oss-fuzz-build.sh @@ -0,0 +1,47 @@ +#!/bin/bash -eu + +git clone https://github.com/dvyukov/go-fuzz-corpus +zip corpus.zip go-fuzz-corpus/json/corpus/* + +cp corpus.zip $OUT/fuzzparsestring_seed_corpus.zip +compile_go_fuzzer github.com/buger/jsonparser FuzzParseString fuzzparsestring + +cp corpus.zip $OUT/fuzzeachkey_seed_corpus.zip +compile_go_fuzzer github.com/buger/jsonparser FuzzEachKey fuzzeachkey + +cp corpus.zip $OUT/fuzzdelete_seed_corpus.zip +compile_go_fuzzer github.com/buger/jsonparser FuzzDelete fuzzdelete + +cp corpus.zip $OUT/fuzzset_seed_corpus.zip +compile_go_fuzzer github.com/buger/jsonparser FuzzSet fuzzset + +cp corpus.zip $OUT/fuzzobjecteach_seed_corpus.zip +compile_go_fuzzer github.com/buger/jsonparser FuzzObjectEach fuzzobjecteach + +cp corpus.zip $OUT/fuzzparsefloat_seed_corpus.zip +compile_go_fuzzer github.com/buger/jsonparser FuzzParseFloat fuzzparsefloat + +cp corpus.zip $OUT/fuzzparseint_seed_corpus.zip +compile_go_fuzzer github.com/buger/jsonparser FuzzParseInt fuzzparseint + +cp corpus.zip $OUT/fuzzparsebool_seed_corpus.zip +compile_go_fuzzer github.com/buger/jsonparser FuzzParseBool fuzzparsebool + +cp corpus.zip $OUT/fuzztokenstart_seed_corpus.zip +compile_go_fuzzer github.com/buger/jsonparser FuzzTokenStart fuzztokenstart + +cp corpus.zip $OUT/fuzzgetstring_seed_corpus.zip +compile_go_fuzzer github.com/buger/jsonparser FuzzGetString fuzzgetstring + +cp corpus.zip $OUT/fuzzgetfloat_seed_corpus.zip +compile_go_fuzzer github.com/buger/jsonparser FuzzGetFloat fuzzgetfloat + +cp corpus.zip $OUT/fuzzgetint_seed_corpus.zip +compile_go_fuzzer github.com/buger/jsonparser FuzzGetInt fuzzgetint + +cp corpus.zip $OUT/fuzzgetboolean_seed_corpus.zip +compile_go_fuzzer github.com/buger/jsonparser FuzzGetBoolean fuzzgetboolean + +cp corpus.zip $OUT/fuzzgetunsafestring_seed_corpus.zip +compile_go_fuzzer github.com/buger/jsonparser FuzzGetUnsafeString fuzzgetunsafestring + diff --git a/vendor/github.com/buger/jsonparser/parser.go b/vendor/github.com/buger/jsonparser/parser.go new file mode 100644 index 000000000..14b80bc48 --- /dev/null +++ b/vendor/github.com/buger/jsonparser/parser.go @@ -0,0 +1,1283 @@ +package jsonparser + +import ( + "bytes" + "errors" + "fmt" + "strconv" +) + +// Errors +var ( + KeyPathNotFoundError = errors.New("Key path not found") + UnknownValueTypeError = errors.New("Unknown value type") + MalformedJsonError = errors.New("Malformed JSON error") + MalformedStringError = errors.New("Value is string, but can't find closing '\"' symbol") + MalformedArrayError = errors.New("Value is array, but can't find closing ']' symbol") + MalformedObjectError = errors.New("Value looks like object, but can't find closing '}' symbol") + MalformedValueError = errors.New("Value looks like Number/Boolean/None, but can't find its end: ',' or '}' symbol") + OverflowIntegerError = errors.New("Value is number, but overflowed while parsing") + MalformedStringEscapeError = errors.New("Encountered an invalid escape sequence in a string") +) + +// How much stack space to allocate for unescaping JSON strings; if a string longer +// than this needs to be escaped, it will result in a heap allocation +const unescapeStackBufSize = 64 + +func tokenEnd(data []byte) int { + for i, c := range data { + switch c { + case ' ', '\n', '\r', '\t', ',', '}', ']': + return i + } + } + + return len(data) +} + +func findTokenStart(data []byte, token byte) int { + for i := len(data) - 1; i >= 0; i-- { + switch data[i] { + case token: + return i + case '[', '{': + return 0 + } + } + + return 0 +} + +func findKeyStart(data []byte, key string) (int, error) { + i := 0 + ln := len(data) + if ln > 0 && (data[0] == '{' || data[0] == '[') { + i = 1 + } + var stackbuf [unescapeStackBufSize]byte // stack-allocated array for allocation-free unescaping of small strings + + if ku, err := Unescape(StringToBytes(key), stackbuf[:]); err == nil { + key = bytesToString(&ku) + } + + for i < ln { + switch data[i] { + case '"': + i++ + keyBegin := i + + strEnd, keyEscaped := stringEnd(data[i:]) + if strEnd == -1 { + break + } + i += strEnd + keyEnd := i - 1 + + valueOffset := nextToken(data[i:]) + if valueOffset == -1 { + break + } + + i += valueOffset + + // if string is a key, and key level match + k := data[keyBegin:keyEnd] + // for unescape: if there are no escape sequences, this is cheap; if there are, it is a + // bit more expensive, but causes no allocations unless len(key) > unescapeStackBufSize + if keyEscaped { + if ku, err := Unescape(k, stackbuf[:]); err != nil { + break + } else { + k = ku + } + } + + if data[i] == ':' && len(key) == len(k) && bytesToString(&k) == key { + return keyBegin - 1, nil + } + + case '[': + end := blockEnd(data[i:], data[i], ']') + if end != -1 { + i = i + end + } + case '{': + end := blockEnd(data[i:], data[i], '}') + if end != -1 { + i = i + end + } + } + i++ + } + + return -1, KeyPathNotFoundError +} + +func tokenStart(data []byte) int { + for i := len(data) - 1; i >= 0; i-- { + switch data[i] { + case '\n', '\r', '\t', ',', '{', '[': + return i + } + } + + return 0 +} + +// Find position of next character which is not whitespace +func nextToken(data []byte) int { + for i, c := range data { + switch c { + case ' ', '\n', '\r', '\t': + continue + default: + return i + } + } + + return -1 +} + +// Find position of last character which is not whitespace +func lastToken(data []byte) int { + for i := len(data) - 1; i >= 0; i-- { + switch data[i] { + case ' ', '\n', '\r', '\t': + continue + default: + return i + } + } + + return -1 +} + +// Tries to find the end of string +// Support if string contains escaped quote symbols. +func stringEnd(data []byte) (int, bool) { + escaped := false + for i, c := range data { + if c == '"' { + if !escaped { + return i + 1, false + } else { + j := i - 1 + for { + if j < 0 || data[j] != '\\' { + return i + 1, true // even number of backslashes + } + j-- + if j < 0 || data[j] != '\\' { + break // odd number of backslashes + } + j-- + + } + } + } else if c == '\\' { + escaped = true + } + } + + return -1, escaped +} + +// Find end of the data structure, array or object. +// For array openSym and closeSym will be '[' and ']', for object '{' and '}' +func blockEnd(data []byte, openSym byte, closeSym byte) int { + level := 0 + i := 0 + ln := len(data) + + for i < ln { + switch data[i] { + case '"': // If inside string, skip it + se, _ := stringEnd(data[i+1:]) + if se == -1 { + return -1 + } + i += se + case openSym: // If open symbol, increase level + level++ + case closeSym: // If close symbol, increase level + level-- + + // If we have returned to the original level, we're done + if level == 0 { + return i + 1 + } + } + i++ + } + + return -1 +} + +func searchKeys(data []byte, keys ...string) int { + keyLevel := 0 + level := 0 + i := 0 + ln := len(data) + lk := len(keys) + lastMatched := true + + if lk == 0 { + return 0 + } + + var stackbuf [unescapeStackBufSize]byte // stack-allocated array for allocation-free unescaping of small strings + + for i < ln { + switch data[i] { + case '"': + i++ + keyBegin := i + + strEnd, keyEscaped := stringEnd(data[i:]) + if strEnd == -1 { + return -1 + } + i += strEnd + keyEnd := i - 1 + + valueOffset := nextToken(data[i:]) + if valueOffset == -1 { + return -1 + } + + i += valueOffset + + // if string is a key + if data[i] == ':' { + if level < 1 { + return -1 + } + + key := data[keyBegin:keyEnd] + + // for unescape: if there are no escape sequences, this is cheap; if there are, it is a + // bit more expensive, but causes no allocations unless len(key) > unescapeStackBufSize + var keyUnesc []byte + if !keyEscaped { + keyUnesc = key + } else if ku, err := Unescape(key, stackbuf[:]); err != nil { + return -1 + } else { + keyUnesc = ku + } + + if level <= len(keys) { + if equalStr(&keyUnesc, keys[level-1]) { + lastMatched = true + + // if key level match + if keyLevel == level-1 { + keyLevel++ + // If we found all keys in path + if keyLevel == lk { + return i + 1 + } + } + } else { + lastMatched = false + } + } else { + return -1 + } + } else { + i-- + } + case '{': + + // in case parent key is matched then only we will increase the level otherwise can directly + // can move to the end of this block + if !lastMatched { + end := blockEnd(data[i:], '{', '}') + if end == -1 { + return -1 + } + i += end - 1 + } else { + level++ + } + case '}': + level-- + if level == keyLevel { + keyLevel-- + } + case '[': + // If we want to get array element by index + if keyLevel == level && keys[level][0] == '[' { + var keyLen = len(keys[level]) + if keyLen < 3 || keys[level][0] != '[' || keys[level][keyLen-1] != ']' { + return -1 + } + aIdx, err := strconv.Atoi(keys[level][1 : keyLen-1]) + if err != nil { + return -1 + } + var curIdx int + var valueFound []byte + var valueOffset int + var curI = i + ArrayEach(data[i:], func(value []byte, dataType ValueType, offset int, err error) { + if curIdx == aIdx { + valueFound = value + valueOffset = offset + if dataType == String { + valueOffset = valueOffset - 2 + valueFound = data[curI+valueOffset : curI+valueOffset+len(value)+2] + } + } + curIdx += 1 + }) + + if valueFound == nil { + return -1 + } else { + subIndex := searchKeys(valueFound, keys[level+1:]...) + if subIndex < 0 { + return -1 + } + return i + valueOffset + subIndex + } + } else { + // Do not search for keys inside arrays + if arraySkip := blockEnd(data[i:], '[', ']'); arraySkip == -1 { + return -1 + } else { + i += arraySkip - 1 + } + } + case ':': // If encountered, JSON data is malformed + return -1 + } + + i++ + } + + return -1 +} + +func sameTree(p1, p2 []string) bool { + minLen := len(p1) + if len(p2) < minLen { + minLen = len(p2) + } + + for pi_1, p_1 := range p1[:minLen] { + if p2[pi_1] != p_1 { + return false + } + } + + return true +} + +func EachKey(data []byte, cb func(int, []byte, ValueType, error), paths ...[]string) int { + var x struct{} + pathFlags := make([]bool, len(paths)) + var level, pathsMatched, i int + ln := len(data) + + var maxPath int + for _, p := range paths { + if len(p) > maxPath { + maxPath = len(p) + } + } + + pathsBuf := make([]string, maxPath) + + for i < ln { + switch data[i] { + case '"': + i++ + keyBegin := i + + strEnd, keyEscaped := stringEnd(data[i:]) + if strEnd == -1 { + return -1 + } + i += strEnd + + keyEnd := i - 1 + + valueOffset := nextToken(data[i:]) + if valueOffset == -1 { + return -1 + } + + i += valueOffset + + // if string is a key, and key level match + if data[i] == ':' { + match := -1 + key := data[keyBegin:keyEnd] + + // for unescape: if there are no escape sequences, this is cheap; if there are, it is a + // bit more expensive, but causes no allocations unless len(key) > unescapeStackBufSize + var keyUnesc []byte + if !keyEscaped { + keyUnesc = key + } else { + var stackbuf [unescapeStackBufSize]byte + if ku, err := Unescape(key, stackbuf[:]); err != nil { + return -1 + } else { + keyUnesc = ku + } + } + + if maxPath >= level { + if level < 1 { + cb(-1, nil, Unknown, MalformedJsonError) + return -1 + } + + pathsBuf[level-1] = bytesToString(&keyUnesc) + for pi, p := range paths { + if len(p) != level || pathFlags[pi] || !equalStr(&keyUnesc, p[level-1]) || !sameTree(p, pathsBuf[:level]) { + continue + } + + match = pi + + pathsMatched++ + pathFlags[pi] = true + + v, dt, _, e := Get(data[i+1:]) + cb(pi, v, dt, e) + + if pathsMatched == len(paths) { + break + } + } + if pathsMatched == len(paths) { + return i + } + } + + if match == -1 { + tokenOffset := nextToken(data[i+1:]) + i += tokenOffset + + if data[i] == '{' { + blockSkip := blockEnd(data[i:], '{', '}') + i += blockSkip + 1 + } + } + + if i < ln { + switch data[i] { + case '{', '}', '[', '"': + i-- + } + } + } else { + i-- + } + case '{': + level++ + case '}': + level-- + case '[': + var ok bool + arrIdxFlags := make(map[int]struct{}) + pIdxFlags := make([]bool, len(paths)) + + if level < 0 { + cb(-1, nil, Unknown, MalformedJsonError) + return -1 + } + + for pi, p := range paths { + if len(p) < level+1 || pathFlags[pi] || p[level][0] != '[' || !sameTree(p, pathsBuf[:level]) { + continue + } + if len(p[level]) >= 2 { + aIdx, _ := strconv.Atoi(p[level][1 : len(p[level])-1]) + arrIdxFlags[aIdx] = x + pIdxFlags[pi] = true + } + } + + if len(arrIdxFlags) > 0 { + level++ + + var curIdx int + arrOff, _ := ArrayEach(data[i:], func(value []byte, dataType ValueType, offset int, err error) { + if _, ok = arrIdxFlags[curIdx]; ok { + for pi, p := range paths { + if pIdxFlags[pi] { + aIdx, _ := strconv.Atoi(p[level-1][1 : len(p[level-1])-1]) + + if curIdx == aIdx { + of := searchKeys(value, p[level:]...) + + pathsMatched++ + pathFlags[pi] = true + + if of != -1 { + v, dt, _, e := Get(value[of:]) + cb(pi, v, dt, e) + } + } + } + } + } + + curIdx += 1 + }) + + if pathsMatched == len(paths) { + return i + } + + i += arrOff - 1 + } else { + // Do not search for keys inside arrays + if arraySkip := blockEnd(data[i:], '[', ']'); arraySkip == -1 { + return -1 + } else { + i += arraySkip - 1 + } + } + case ']': + level-- + } + + i++ + } + + return -1 +} + +// Data types available in valid JSON data. +type ValueType int + +const ( + NotExist = ValueType(iota) + String + Number + Object + Array + Boolean + Null + Unknown +) + +func (vt ValueType) String() string { + switch vt { + case NotExist: + return "non-existent" + case String: + return "string" + case Number: + return "number" + case Object: + return "object" + case Array: + return "array" + case Boolean: + return "boolean" + case Null: + return "null" + default: + return "unknown" + } +} + +var ( + trueLiteral = []byte("true") + falseLiteral = []byte("false") + nullLiteral = []byte("null") +) + +func createInsertComponent(keys []string, setValue []byte, comma, object bool) []byte { + isIndex := string(keys[0][0]) == "[" + offset := 0 + lk := calcAllocateSpace(keys, setValue, comma, object) + buffer := make([]byte, lk, lk) + if comma { + offset += WriteToBuffer(buffer[offset:], ",") + } + if isIndex && !comma { + offset += WriteToBuffer(buffer[offset:], "[") + } else { + if object { + offset += WriteToBuffer(buffer[offset:], "{") + } + if !isIndex { + offset += WriteToBuffer(buffer[offset:], "\"") + offset += WriteToBuffer(buffer[offset:], keys[0]) + offset += WriteToBuffer(buffer[offset:], "\":") + } + } + + for i := 1; i < len(keys); i++ { + if string(keys[i][0]) == "[" { + offset += WriteToBuffer(buffer[offset:], "[") + } else { + offset += WriteToBuffer(buffer[offset:], "{\"") + offset += WriteToBuffer(buffer[offset:], keys[i]) + offset += WriteToBuffer(buffer[offset:], "\":") + } + } + offset += WriteToBuffer(buffer[offset:], string(setValue)) + for i := len(keys) - 1; i > 0; i-- { + if string(keys[i][0]) == "[" { + offset += WriteToBuffer(buffer[offset:], "]") + } else { + offset += WriteToBuffer(buffer[offset:], "}") + } + } + if isIndex && !comma { + offset += WriteToBuffer(buffer[offset:], "]") + } + if object && !isIndex { + offset += WriteToBuffer(buffer[offset:], "}") + } + return buffer +} + +func calcAllocateSpace(keys []string, setValue []byte, comma, object bool) int { + isIndex := string(keys[0][0]) == "[" + lk := 0 + if comma { + // , + lk += 1 + } + if isIndex && !comma { + // [] + lk += 2 + } else { + if object { + // { + lk += 1 + } + if !isIndex { + // "keys[0]" + lk += len(keys[0]) + 3 + } + } + + + lk += len(setValue) + for i := 1; i < len(keys); i++ { + if string(keys[i][0]) == "[" { + // [] + lk += 2 + } else { + // {"keys[i]":setValue} + lk += len(keys[i]) + 5 + } + } + + if object && !isIndex { + // } + lk += 1 + } + + return lk +} + +func WriteToBuffer(buffer []byte, str string) int { + copy(buffer, str) + return len(str) +} + +/* + +Del - Receives existing data structure, path to delete. + +Returns: +`data` - return modified data + +*/ +func Delete(data []byte, keys ...string) []byte { + lk := len(keys) + if lk == 0 { + return data[:0] + } + + array := false + if len(keys[lk-1]) > 0 && string(keys[lk-1][0]) == "[" { + array = true + } + + var startOffset, keyOffset int + endOffset := len(data) + var err error + if !array { + if len(keys) > 1 { + _, _, startOffset, endOffset, err = internalGet(data, keys[:lk-1]...) + if err == KeyPathNotFoundError { + // problem parsing the data + return data + } + } + + keyOffset, err = findKeyStart(data[startOffset:endOffset], keys[lk-1]) + if err == KeyPathNotFoundError { + // problem parsing the data + return data + } + keyOffset += startOffset + _, _, _, subEndOffset, _ := internalGet(data[startOffset:endOffset], keys[lk-1]) + endOffset = startOffset + subEndOffset + tokEnd := tokenEnd(data[endOffset:]) + tokStart := findTokenStart(data[:keyOffset], ","[0]) + + if data[endOffset+tokEnd] == ","[0] { + endOffset += tokEnd + 1 + } else if data[endOffset+tokEnd] == " "[0] && len(data) > endOffset+tokEnd+1 && data[endOffset+tokEnd+1] == ","[0] { + endOffset += tokEnd + 2 + } else if data[endOffset+tokEnd] == "}"[0] && data[tokStart] == ","[0] { + keyOffset = tokStart + } + } else { + _, _, keyOffset, endOffset, err = internalGet(data, keys...) + if err == KeyPathNotFoundError { + // problem parsing the data + return data + } + + tokEnd := tokenEnd(data[endOffset:]) + tokStart := findTokenStart(data[:keyOffset], ","[0]) + + if data[endOffset+tokEnd] == ","[0] { + endOffset += tokEnd + 1 + } else if data[endOffset+tokEnd] == "]"[0] && data[tokStart] == ","[0] { + keyOffset = tokStart + } + } + + // We need to remove remaining trailing comma if we delete las element in the object + prevTok := lastToken(data[:keyOffset]) + remainedValue := data[endOffset:] + + var newOffset int + if nextToken(remainedValue) > -1 && remainedValue[nextToken(remainedValue)] == '}' && data[prevTok] == ',' { + newOffset = prevTok + } else { + newOffset = prevTok + 1 + } + + // We have to make a copy here if we don't want to mangle the original data, because byte slices are + // accessed by reference and not by value + dataCopy := make([]byte, len(data)) + copy(dataCopy, data) + data = append(dataCopy[:newOffset], dataCopy[endOffset:]...) + + return data +} + +/* + +Set - Receives existing data structure, path to set, and data to set at that key. + +Returns: +`value` - modified byte array +`err` - On any parsing error + +*/ +func Set(data []byte, setValue []byte, keys ...string) (value []byte, err error) { + // ensure keys are set + if len(keys) == 0 { + return nil, KeyPathNotFoundError + } + + _, _, startOffset, endOffset, err := internalGet(data, keys...) + if err != nil { + if err != KeyPathNotFoundError { + // problem parsing the data + return nil, err + } + // full path doesnt exist + // does any subpath exist? + var depth int + for i := range keys { + _, _, start, end, sErr := internalGet(data, keys[:i+1]...) + if sErr != nil { + break + } else { + endOffset = end + startOffset = start + depth++ + } + } + comma := true + object := false + if endOffset == -1 { + firstToken := nextToken(data) + // We can't set a top-level key if data isn't an object + if firstToken < 0 || data[firstToken] != '{' { + return nil, KeyPathNotFoundError + } + // Don't need a comma if the input is an empty object + secondToken := firstToken + 1 + nextToken(data[firstToken+1:]) + if data[secondToken] == '}' { + comma = false + } + // Set the top level key at the end (accounting for any trailing whitespace) + // This assumes last token is valid like '}', could check and return error + endOffset = lastToken(data) + } + depthOffset := endOffset + if depth != 0 { + // if subpath is a non-empty object, add to it + // or if subpath is a non-empty array, add to it + if (data[startOffset] == '{' && data[startOffset+1+nextToken(data[startOffset+1:])] != '}') || + (data[startOffset] == '[' && data[startOffset+1+nextToken(data[startOffset+1:])] == '{') && keys[depth:][0][0] == 91 { + depthOffset-- + startOffset = depthOffset + // otherwise, over-write it with a new object + } else { + comma = false + object = true + } + } else { + startOffset = depthOffset + } + value = append(data[:startOffset], append(createInsertComponent(keys[depth:], setValue, comma, object), data[depthOffset:]...)...) + } else { + // path currently exists + startComponent := data[:startOffset] + endComponent := data[endOffset:] + + value = make([]byte, len(startComponent)+len(endComponent)+len(setValue)) + newEndOffset := startOffset + len(setValue) + copy(value[0:startOffset], startComponent) + copy(value[startOffset:newEndOffset], setValue) + copy(value[newEndOffset:], endComponent) + } + return value, nil +} + +func getType(data []byte, offset int) ([]byte, ValueType, int, error) { + var dataType ValueType + endOffset := offset + + // if string value + if data[offset] == '"' { + dataType = String + if idx, _ := stringEnd(data[offset+1:]); idx != -1 { + endOffset += idx + 1 + } else { + return nil, dataType, offset, MalformedStringError + } + } else if data[offset] == '[' { // if array value + dataType = Array + // break label, for stopping nested loops + endOffset = blockEnd(data[offset:], '[', ']') + + if endOffset == -1 { + return nil, dataType, offset, MalformedArrayError + } + + endOffset += offset + } else if data[offset] == '{' { // if object value + dataType = Object + // break label, for stopping nested loops + endOffset = blockEnd(data[offset:], '{', '}') + + if endOffset == -1 { + return nil, dataType, offset, MalformedObjectError + } + + endOffset += offset + } else { + // Number, Boolean or None + end := tokenEnd(data[endOffset:]) + + if end == -1 { + return nil, dataType, offset, MalformedValueError + } + + value := data[offset : endOffset+end] + + switch data[offset] { + case 't', 'f': // true or false + if bytes.Equal(value, trueLiteral) || bytes.Equal(value, falseLiteral) { + dataType = Boolean + } else { + return nil, Unknown, offset, UnknownValueTypeError + } + case 'u', 'n': // undefined or null + if bytes.Equal(value, nullLiteral) { + dataType = Null + } else { + return nil, Unknown, offset, UnknownValueTypeError + } + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-': + dataType = Number + default: + return nil, Unknown, offset, UnknownValueTypeError + } + + endOffset += end + } + return data[offset:endOffset], dataType, endOffset, nil +} + +/* +Get - Receives data structure, and key path to extract value from. + +Returns: +`value` - Pointer to original data structure containing key value, or just empty slice if nothing found or error +`dataType` - Can be: `NotExist`, `String`, `Number`, `Object`, `Array`, `Boolean` or `Null` +`offset` - Offset from provided data structure where key value ends. Used mostly internally, for example for `ArrayEach` helper. +`err` - If key not found or any other parsing issue it should return error. If key not found it also sets `dataType` to `NotExist` + +Accept multiple keys to specify path to JSON value (in case of quering nested structures). +If no keys provided it will try to extract closest JSON value (simple ones or object/array), useful for reading streams or arrays, see `ArrayEach` implementation. +*/ +func Get(data []byte, keys ...string) (value []byte, dataType ValueType, offset int, err error) { + a, b, _, d, e := internalGet(data, keys...) + return a, b, d, e +} + +func internalGet(data []byte, keys ...string) (value []byte, dataType ValueType, offset, endOffset int, err error) { + if len(keys) > 0 { + if offset = searchKeys(data, keys...); offset == -1 { + return nil, NotExist, -1, -1, KeyPathNotFoundError + } + } + + // Go to closest value + nO := nextToken(data[offset:]) + if nO == -1 { + return nil, NotExist, offset, -1, MalformedJsonError + } + + offset += nO + value, dataType, endOffset, err = getType(data, offset) + if err != nil { + return value, dataType, offset, endOffset, err + } + + // Strip quotes from string values + if dataType == String { + value = value[1 : len(value)-1] + } + + return value[:len(value):len(value)], dataType, offset, endOffset, nil +} + +// ArrayEach is used when iterating arrays, accepts a callback function with the same return arguments as `Get`. +func ArrayEach(data []byte, cb func(value []byte, dataType ValueType, offset int, err error), keys ...string) (offset int, err error) { + if len(data) == 0 { + return -1, MalformedObjectError + } + + nT := nextToken(data) + if nT == -1 { + return -1, MalformedJsonError + } + + offset = nT + 1 + + if len(keys) > 0 { + if offset = searchKeys(data, keys...); offset == -1 { + return offset, KeyPathNotFoundError + } + + // Go to closest value + nO := nextToken(data[offset:]) + if nO == -1 { + return offset, MalformedJsonError + } + + offset += nO + + if data[offset] != '[' { + return offset, MalformedArrayError + } + + offset++ + } + + nO := nextToken(data[offset:]) + if nO == -1 { + return offset, MalformedJsonError + } + + offset += nO + + if data[offset] == ']' { + return offset, nil + } + + for true { + v, t, o, e := Get(data[offset:]) + + if e != nil { + return offset, e + } + + if o == 0 { + break + } + + if t != NotExist { + cb(v, t, offset+o-len(v), e) + } + + if e != nil { + break + } + + offset += o + + skipToToken := nextToken(data[offset:]) + if skipToToken == -1 { + return offset, MalformedArrayError + } + offset += skipToToken + + if data[offset] == ']' { + break + } + + if data[offset] != ',' { + return offset, MalformedArrayError + } + + offset++ + } + + return offset, nil +} + +// ObjectEach iterates over the key-value pairs of a JSON object, invoking a given callback for each such entry +func ObjectEach(data []byte, callback func(key []byte, value []byte, dataType ValueType, offset int) error, keys ...string) (err error) { + offset := 0 + + // Descend to the desired key, if requested + if len(keys) > 0 { + if off := searchKeys(data, keys...); off == -1 { + return KeyPathNotFoundError + } else { + offset = off + } + } + + // Validate and skip past opening brace + if off := nextToken(data[offset:]); off == -1 { + return MalformedObjectError + } else if offset += off; data[offset] != '{' { + return MalformedObjectError + } else { + offset++ + } + + // Skip to the first token inside the object, or stop if we find the ending brace + if off := nextToken(data[offset:]); off == -1 { + return MalformedJsonError + } else if offset += off; data[offset] == '}' { + return nil + } + + // Loop pre-condition: data[offset] points to what should be either the next entry's key, or the closing brace (if it's anything else, the JSON is malformed) + for offset < len(data) { + // Step 1: find the next key + var key []byte + + // Check what the the next token is: start of string, end of object, or something else (error) + switch data[offset] { + case '"': + offset++ // accept as string and skip opening quote + case '}': + return nil // we found the end of the object; stop and return success + default: + return MalformedObjectError + } + + // Find the end of the key string + var keyEscaped bool + if off, esc := stringEnd(data[offset:]); off == -1 { + return MalformedJsonError + } else { + key, keyEscaped = data[offset:offset+off-1], esc + offset += off + } + + // Unescape the string if needed + if keyEscaped { + var stackbuf [unescapeStackBufSize]byte // stack-allocated array for allocation-free unescaping of small strings + if keyUnescaped, err := Unescape(key, stackbuf[:]); err != nil { + return MalformedStringEscapeError + } else { + key = keyUnescaped + } + } + + // Step 2: skip the colon + if off := nextToken(data[offset:]); off == -1 { + return MalformedJsonError + } else if offset += off; data[offset] != ':' { + return MalformedJsonError + } else { + offset++ + } + + // Step 3: find the associated value, then invoke the callback + if value, valueType, off, err := Get(data[offset:]); err != nil { + return err + } else if err := callback(key, value, valueType, offset+off); err != nil { // Invoke the callback here! + return err + } else { + offset += off + } + + // Step 4: skip over the next comma to the following token, or stop if we hit the ending brace + if off := nextToken(data[offset:]); off == -1 { + return MalformedArrayError + } else { + offset += off + switch data[offset] { + case '}': + return nil // Stop if we hit the close brace + case ',': + offset++ // Ignore the comma + default: + return MalformedObjectError + } + } + + // Skip to the next token after the comma + if off := nextToken(data[offset:]); off == -1 { + return MalformedArrayError + } else { + offset += off + } + } + + return MalformedObjectError // we shouldn't get here; it's expected that we will return via finding the ending brace +} + +// GetUnsafeString returns the value retrieved by `Get`, use creates string without memory allocation by mapping string to slice memory. It does not handle escape symbols. +func GetUnsafeString(data []byte, keys ...string) (val string, err error) { + v, _, _, e := Get(data, keys...) + + if e != nil { + return "", e + } + + return bytesToString(&v), nil +} + +// GetString returns the value retrieved by `Get`, cast to a string if possible, trying to properly handle escape and utf8 symbols +// If key data type do not match, it will return an error. +func GetString(data []byte, keys ...string) (val string, err error) { + v, t, _, e := Get(data, keys...) + + if e != nil { + return "", e + } + + if t != String { + return "", fmt.Errorf("Value is not a string: %s", string(v)) + } + + // If no escapes return raw content + if bytes.IndexByte(v, '\\') == -1 { + return string(v), nil + } + + return ParseString(v) +} + +// GetFloat returns the value retrieved by `Get`, cast to a float64 if possible. +// The offset is the same as in `Get`. +// If key data type do not match, it will return an error. +func GetFloat(data []byte, keys ...string) (val float64, err error) { + v, t, _, e := Get(data, keys...) + + if e != nil { + return 0, e + } + + if t != Number { + return 0, fmt.Errorf("Value is not a number: %s", string(v)) + } + + return ParseFloat(v) +} + +// GetInt returns the value retrieved by `Get`, cast to a int64 if possible. +// If key data type do not match, it will return an error. +func GetInt(data []byte, keys ...string) (val int64, err error) { + v, t, _, e := Get(data, keys...) + + if e != nil { + return 0, e + } + + if t != Number { + return 0, fmt.Errorf("Value is not a number: %s", string(v)) + } + + return ParseInt(v) +} + +// GetBoolean returns the value retrieved by `Get`, cast to a bool if possible. +// The offset is the same as in `Get`. +// If key data type do not match, it will return error. +func GetBoolean(data []byte, keys ...string) (val bool, err error) { + v, t, _, e := Get(data, keys...) + + if e != nil { + return false, e + } + + if t != Boolean { + return false, fmt.Errorf("Value is not a boolean: %s", string(v)) + } + + return ParseBoolean(v) +} + +// ParseBoolean parses a Boolean ValueType into a Go bool (not particularly useful, but here for completeness) +func ParseBoolean(b []byte) (bool, error) { + switch { + case bytes.Equal(b, trueLiteral): + return true, nil + case bytes.Equal(b, falseLiteral): + return false, nil + default: + return false, MalformedValueError + } +} + +// ParseString parses a String ValueType into a Go string (the main parsing work is unescaping the JSON string) +func ParseString(b []byte) (string, error) { + var stackbuf [unescapeStackBufSize]byte // stack-allocated array for allocation-free unescaping of small strings + if bU, err := Unescape(b, stackbuf[:]); err != nil { + return "", MalformedValueError + } else { + return string(bU), nil + } +} + +// ParseNumber parses a Number ValueType into a Go float64 +func ParseFloat(b []byte) (float64, error) { + if v, err := parseFloat(&b); err != nil { + return 0, MalformedValueError + } else { + return v, nil + } +} + +// ParseInt parses a Number ValueType into a Go int64 +func ParseInt(b []byte) (int64, error) { + if v, ok, overflow := parseInt(b); !ok { + if overflow { + return 0, OverflowIntegerError + } + return 0, MalformedValueError + } else { + return v, nil + } +} diff --git a/vendor/github.com/uptrace/bun/CHANGELOG.md b/vendor/github.com/uptrace/bun/CHANGELOG.md index ded6f1f40..82ee95a83 100644 --- a/vendor/github.com/uptrace/bun/CHANGELOG.md +++ b/vendor/github.com/uptrace/bun/CHANGELOG.md @@ -1,3 +1,52 @@ +## [1.2.6](https://github.com/uptrace/bun/compare/v1.2.5...v1.2.6) (2024-11-20) + + +### Bug Fixes + +* append IDENTITY to ADD COLUMN statement if needed ([694f873](https://github.com/uptrace/bun/commit/694f873d61ed8d2f09032ae0c0dbec4b71c3719e)) +* **ci:** prune stale should be executed at 3 AM every day ([0cedcb0](https://github.com/uptrace/bun/commit/0cedcb068229b63041a4f48de12bb767c8454048)) +* cleanup after testUniqueRenamedTable ([b1ae32e](https://github.com/uptrace/bun/commit/b1ae32e9e9f45ff2a66e50bfd13bedcf6653d874)) +* fix go.mod of oracledialect ([89e21ea](https://github.com/uptrace/bun/commit/89e21eab362c60511cca00890ae29551a2ba7c46)) +* has many relationship with multiple columns ([1664b2c](https://github.com/uptrace/bun/commit/1664b2c07a5f6cfd3b6730e5005373686e9830a6)) +* ignore case for type equivalence ([c3253a5](https://github.com/uptrace/bun/commit/c3253a5c59b078607db9e216ddc11afdef546e05)) +* implement DefaultSchema for Oracle dialect ([d08fa40](https://github.com/uptrace/bun/commit/d08fa40cc87d67296a83a77448ea511531fc8cdd)) +* **oracledialect:** add go.mod file so the dialect is released properly ([#1043](https://github.com/uptrace/bun/issues/1043)) ([1bb5597](https://github.com/uptrace/bun/commit/1bb5597f1a32f5d693101ef4a62e25d99f5b9db5)) +* **oracledialect:** update go.mod by go mod tidy to fix tests ([7f90a15](https://github.com/uptrace/bun/commit/7f90a15c51a2482dda94226dd13b913d6b470a29)) +* **pgdialect:** array value quoting ([892c416](https://github.com/uptrace/bun/commit/892c416272a8428c592896d65d3ad51a6f2356d8)) +* remove schema name from t.Name during bun-schema inspection ([31ed582](https://github.com/uptrace/bun/commit/31ed58254ad08143d88684672acd33ce044ea5a9)) +* rename column only if the name does not exist in 'target' ([fed6012](https://github.com/uptrace/bun/commit/fed6012d177e55b8320b31ef37fc02a0cbf0b9f5)) +* support embed with tag Unique ([3acd6dd](https://github.com/uptrace/bun/commit/3acd6dd8546118d7b867ca796a5e56311edad070)) +* update oracledialect/version.go in release.sh ([bcd070f](https://github.com/uptrace/bun/commit/bcd070f48a75d0092a5620261658c9c5994f0bf6)) +* update schema.Field names ([9b810de](https://github.com/uptrace/bun/commit/9b810dee4b1a721efb82c913099f39f52c44eb57)) + + +### Features + +* add and drop columns ([3fdd5b8](https://github.com/uptrace/bun/commit/3fdd5b8f635f849a74e78c665274609f75245b19)) +* add and drop IDENTITY ([dd83779](https://github.com/uptrace/bun/commit/dd837795c31490fd8816eec0e9833e79fafdda32)) +* add support type for net/netip.addr and net/netip.prefix ([#1028](https://github.com/uptrace/bun/issues/1028)) ([95c4a8e](https://github.com/uptrace/bun/commit/95c4a8ebd634e1e99114727a7b157eeeb9297ee9)) +* **automigrate:** detect renamed tables ([c03938f](https://github.com/uptrace/bun/commit/c03938ff5e9fa2f653e4c60668b1368357d2de10)) +* change column type ([3cfd8c6](https://github.com/uptrace/bun/commit/3cfd8c62125786aaf6f493acc5b39f4d3db3d628)) +* **ci:** support release on osx ([435510b](https://github.com/uptrace/bun/commit/435510b0a73b0d9e6d06e3e3c3f0fa4379e9ed8c)) +* create sql migrations and apply them ([1bf7cfd](https://github.com/uptrace/bun/commit/1bf7cfd067e0e26ae212b0f7421e5abc6f67fb4f)) +* create transactional migration files ([c3320f6](https://github.com/uptrace/bun/commit/c3320f624830dc2fe99af2c7cbe492b2a83f9e4a)) +* detect Create/Drop table ([408859f](https://github.com/uptrace/bun/commit/408859f07be38236b39a00909cdce55d49f6f824)) +* detect modified relations ([a918dc4](https://github.com/uptrace/bun/commit/a918dc472a33dd24c5fffd4d048bcf49f2e07a42)) +* detect renamed columns ([886d0a5](https://github.com/uptrace/bun/commit/886d0a5b18aba272f1c86af2a2cf68ce4c8879f2)) +* detect renamed tables ([8857bab](https://github.com/uptrace/bun/commit/8857bab54b94170d218633f3b210d379e4e51a21)) +* enhance Apply method to accept multiple functions ([7823f2f](https://github.com/uptrace/bun/commit/7823f2f24c814e104dc59475156255c7b3b26144)) +* implement fmt.Stringer queries ([5060e47](https://github.com/uptrace/bun/commit/5060e47db13451a982e48d0f14055a58ba60b472)) +* improve FK handling ([a822fc5](https://github.com/uptrace/bun/commit/a822fc5f8ae547b7cd41e1ca35609d519d78598b)) +* include target schema name in migration name ([ac8d221](https://github.com/uptrace/bun/commit/ac8d221e6443b469e794314c5fc189250fa542d5)) +* **mariadb:** support RETURNING clause in DELETE statement ([b8dec9d](https://github.com/uptrace/bun/commit/b8dec9d9a06124696bd5ee2abbf33f19087174b6)) +* migrate FKs ([4c1dfdb](https://github.com/uptrace/bun/commit/4c1dfdbe99c73d0c0f2d7b1f8b11adf30c6a41f7)) +* **mysql:** support ORDER BY and LIMIT clauses in UPDATE and DELETE statements ([de71bed](https://github.com/uptrace/bun/commit/de71bed9252980648269af85b7a51cbc464ce710)) +* support modifying primary keys ([a734629](https://github.com/uptrace/bun/commit/a734629fa285406038cbe4a50798626b5ac08539)) +* support UNIQUE constraints ([3c4d5d2](https://github.com/uptrace/bun/commit/3c4d5d2c47be4652fb9b5cf1c6bd7b6c0a437287)) +* use *bun.DB in MigratorDialect ([a8788bf](https://github.com/uptrace/bun/commit/a8788bf62cbcc954a08532c299c774262de7a81d)) + + + ## [1.2.5](https://github.com/uptrace/bun/compare/v1.2.3...v1.2.5) (2024-10-26) diff --git a/vendor/github.com/uptrace/bun/Makefile b/vendor/github.com/uptrace/bun/Makefile index 50a1903e7..255d0f7ee 100644 --- a/vendor/github.com/uptrace/bun/Makefile +++ b/vendor/github.com/uptrace/bun/Makefile @@ -6,7 +6,7 @@ test: echo "go test in $${dir}"; \ (cd "$${dir}" && \ go test && \ - env GOOS=linux GOARCH=386 go test && \ + env GOOS=linux GOARCH=386 TZ= go test && \ go vet); \ done diff --git a/vendor/github.com/uptrace/bun/README.md b/vendor/github.com/uptrace/bun/README.md index dbe5bc0b4..eb2d98bc5 100644 --- a/vendor/github.com/uptrace/bun/README.md +++ b/vendor/github.com/uptrace/bun/README.md @@ -4,6 +4,7 @@ [![PkgGoDev](https://pkg.go.dev/badge/github.com/uptrace/bun)](https://pkg.go.dev/github.com/uptrace/bun) [![Documentation](https://img.shields.io/badge/bun-documentation-informational)](https://bun.uptrace.dev/) [![Chat](https://discordapp.com/api/guilds/752070105847955518/widget.png)](https://discord.gg/rWtp5Aj) +[![Gurubase](https://img.shields.io/badge/Gurubase-Ask%20Bun%20Guru-006BFF)](https://gurubase.io/g/bun) > Bun is brought to you by :star: [**uptrace/uptrace**](https://github.com/uptrace/uptrace). Uptrace > is an open-source APM tool that supports distributed tracing, metrics, and logs. You can use it to diff --git a/vendor/github.com/uptrace/bun/db.go b/vendor/github.com/uptrace/bun/db.go index 106dfe905..2f52a2248 100644 --- a/vendor/github.com/uptrace/bun/db.go +++ b/vendor/github.com/uptrace/bun/db.go @@ -703,6 +703,5 @@ func (tx Tx) NewDropColumn() *DropColumnQuery { //------------------------------------------------------------------------------ func (db *DB) makeQueryBytes() []byte { - // TODO: make this configurable? - return make([]byte, 0, 4096) + return internal.MakeQueryBytes() } diff --git a/vendor/github.com/uptrace/bun/dialect/append.go b/vendor/github.com/uptrace/bun/dialect/append.go index 48f092284..8f5485fe3 100644 --- a/vendor/github.com/uptrace/bun/dialect/append.go +++ b/vendor/github.com/uptrace/bun/dialect/append.go @@ -25,24 +25,24 @@ func AppendBool(b []byte, v bool) []byte { return append(b, "FALSE"...) } -func AppendFloat32(b []byte, v float32) []byte { - return appendFloat(b, float64(v), 32) +func AppendFloat32(b []byte, num float32) []byte { + return appendFloat(b, float64(num), 32) } -func AppendFloat64(b []byte, v float64) []byte { - return appendFloat(b, v, 64) +func AppendFloat64(b []byte, num float64) []byte { + return appendFloat(b, num, 64) } -func appendFloat(b []byte, v float64, bitSize int) []byte { +func appendFloat(b []byte, num float64, bitSize int) []byte { switch { - case math.IsNaN(v): + case math.IsNaN(num): return append(b, "'NaN'"...) - case math.IsInf(v, 1): + case math.IsInf(num, 1): return append(b, "'Infinity'"...) - case math.IsInf(v, -1): + case math.IsInf(num, -1): return append(b, "'-Infinity'"...) default: - return strconv.AppendFloat(b, v, 'f', -1, bitSize) + return strconv.AppendFloat(b, num, 'f', -1, bitSize) } } diff --git a/vendor/github.com/uptrace/bun/dialect/feature/feature.go b/vendor/github.com/uptrace/bun/dialect/feature/feature.go index e311394d5..0707c6f88 100644 --- a/vendor/github.com/uptrace/bun/dialect/feature/feature.go +++ b/vendor/github.com/uptrace/bun/dialect/feature/feature.go @@ -31,5 +31,8 @@ const ( UpdateFromTable MSSavepoint GeneratedIdentity - CompositeIn // ... WHERE (A,B) IN ((N, NN), (N, NN)...) + CompositeIn // ... WHERE (A,B) IN ((N, NN), (N, NN)...) + UpdateOrderLimit // UPDATE ... ORDER BY ... LIMIT ... + DeleteOrderLimit // DELETE ... ORDER BY ... LIMIT ... + DeleteReturning ) diff --git a/vendor/github.com/uptrace/bun/dialect/pgdialect/alter_table.go b/vendor/github.com/uptrace/bun/dialect/pgdialect/alter_table.go new file mode 100644 index 000000000..d20f8c069 --- /dev/null +++ b/vendor/github.com/uptrace/bun/dialect/pgdialect/alter_table.go @@ -0,0 +1,245 @@ +package pgdialect + +import ( + "fmt" + "strings" + + "github.com/uptrace/bun" + "github.com/uptrace/bun/migrate" + "github.com/uptrace/bun/migrate/sqlschema" + "github.com/uptrace/bun/schema" +) + +func (d *Dialect) NewMigrator(db *bun.DB, schemaName string) sqlschema.Migrator { + return &migrator{db: db, schemaName: schemaName, BaseMigrator: sqlschema.NewBaseMigrator(db)} +} + +type migrator struct { + *sqlschema.BaseMigrator + + db *bun.DB + schemaName string +} + +var _ sqlschema.Migrator = (*migrator)(nil) + +func (m *migrator) AppendSQL(b []byte, operation interface{}) (_ []byte, err error) { + fmter := m.db.Formatter() + + // Append ALTER TABLE statement to the enclosed query bytes []byte. + appendAlterTable := func(query []byte, tableName string) []byte { + query = append(query, "ALTER TABLE "...) + query = m.appendFQN(fmter, query, tableName) + return append(query, " "...) + } + + switch change := operation.(type) { + case *migrate.CreateTableOp: + return m.AppendCreateTable(b, change.Model) + case *migrate.DropTableOp: + return m.AppendDropTable(b, m.schemaName, change.TableName) + case *migrate.RenameTableOp: + b, err = m.renameTable(fmter, appendAlterTable(b, change.TableName), change) + case *migrate.RenameColumnOp: + b, err = m.renameColumn(fmter, appendAlterTable(b, change.TableName), change) + case *migrate.AddColumnOp: + b, err = m.addColumn(fmter, appendAlterTable(b, change.TableName), change) + case *migrate.DropColumnOp: + b, err = m.dropColumn(fmter, appendAlterTable(b, change.TableName), change) + case *migrate.AddPrimaryKeyOp: + b, err = m.addPrimaryKey(fmter, appendAlterTable(b, change.TableName), change.PrimaryKey) + case *migrate.ChangePrimaryKeyOp: + b, err = m.changePrimaryKey(fmter, appendAlterTable(b, change.TableName), change) + case *migrate.DropPrimaryKeyOp: + b, err = m.dropConstraint(fmter, appendAlterTable(b, change.TableName), change.PrimaryKey.Name) + case *migrate.AddUniqueConstraintOp: + b, err = m.addUnique(fmter, appendAlterTable(b, change.TableName), change) + case *migrate.DropUniqueConstraintOp: + b, err = m.dropConstraint(fmter, appendAlterTable(b, change.TableName), change.Unique.Name) + case *migrate.ChangeColumnTypeOp: + b, err = m.changeColumnType(fmter, appendAlterTable(b, change.TableName), change) + case *migrate.AddForeignKeyOp: + b, err = m.addForeignKey(fmter, appendAlterTable(b, change.TableName()), change) + case *migrate.DropForeignKeyOp: + b, err = m.dropConstraint(fmter, appendAlterTable(b, change.TableName()), change.ConstraintName) + default: + return nil, fmt.Errorf("append sql: unknown operation %T", change) + } + if err != nil { + return nil, fmt.Errorf("append sql: %w", err) + } + return b, nil +} + +func (m *migrator) appendFQN(fmter schema.Formatter, b []byte, tableName string) []byte { + return fmter.AppendQuery(b, "?.?", bun.Ident(m.schemaName), bun.Ident(tableName)) +} + +func (m *migrator) renameTable(fmter schema.Formatter, b []byte, rename *migrate.RenameTableOp) (_ []byte, err error) { + b = append(b, "RENAME TO "...) + b = fmter.AppendName(b, rename.NewName) + return b, nil +} + +func (m *migrator) renameColumn(fmter schema.Formatter, b []byte, rename *migrate.RenameColumnOp) (_ []byte, err error) { + b = append(b, "RENAME COLUMN "...) + b = fmter.AppendName(b, rename.OldName) + + b = append(b, " TO "...) + b = fmter.AppendName(b, rename.NewName) + + return b, nil +} + +func (m *migrator) addColumn(fmter schema.Formatter, b []byte, add *migrate.AddColumnOp) (_ []byte, err error) { + b = append(b, "ADD COLUMN "...) + b = fmter.AppendName(b, add.ColumnName) + b = append(b, " "...) + + b, err = add.Column.AppendQuery(fmter, b) + if err != nil { + return nil, err + } + + if add.Column.GetDefaultValue() != "" { + b = append(b, " DEFAULT "...) + b = append(b, add.Column.GetDefaultValue()...) + b = append(b, " "...) + } + + if add.Column.GetIsIdentity() { + b = appendGeneratedAsIdentity(b) + } + + return b, nil +} + +func (m *migrator) dropColumn(fmter schema.Formatter, b []byte, drop *migrate.DropColumnOp) (_ []byte, err error) { + b = append(b, "DROP COLUMN "...) + b = fmter.AppendName(b, drop.ColumnName) + + return b, nil +} + +func (m *migrator) addPrimaryKey(fmter schema.Formatter, b []byte, pk sqlschema.PrimaryKey) (_ []byte, err error) { + b = append(b, "ADD PRIMARY KEY ("...) + b, _ = pk.Columns.AppendQuery(fmter, b) + b = append(b, ")"...) + + return b, nil +} + +func (m *migrator) changePrimaryKey(fmter schema.Formatter, b []byte, change *migrate.ChangePrimaryKeyOp) (_ []byte, err error) { + b, _ = m.dropConstraint(fmter, b, change.Old.Name) + b = append(b, ", "...) + b, _ = m.addPrimaryKey(fmter, b, change.New) + return b, nil +} + +func (m *migrator) addUnique(fmter schema.Formatter, b []byte, change *migrate.AddUniqueConstraintOp) (_ []byte, err error) { + b = append(b, "ADD CONSTRAINT "...) + if change.Unique.Name != "" { + b = fmter.AppendName(b, change.Unique.Name) + } else { + // Default naming scheme for unique constraints in Postgres is __key + b = fmter.AppendName(b, fmt.Sprintf("%s_%s_key", change.TableName, change.Unique.Columns)) + } + b = append(b, " UNIQUE ("...) + b, _ = change.Unique.Columns.AppendQuery(fmter, b) + b = append(b, ")"...) + + return b, nil +} + +func (m *migrator) dropConstraint(fmter schema.Formatter, b []byte, name string) (_ []byte, err error) { + b = append(b, "DROP CONSTRAINT "...) + b = fmter.AppendName(b, name) + + return b, nil +} + +func (m *migrator) addForeignKey(fmter schema.Formatter, b []byte, add *migrate.AddForeignKeyOp) (_ []byte, err error) { + b = append(b, "ADD CONSTRAINT "...) + + name := add.ConstraintName + if name == "" { + colRef := add.ForeignKey.From + columns := strings.Join(colRef.Column.Split(), "_") + name = fmt.Sprintf("%s_%s_fkey", colRef.TableName, columns) + } + b = fmter.AppendName(b, name) + + b = append(b, " FOREIGN KEY ("...) + if b, err = add.ForeignKey.From.Column.AppendQuery(fmter, b); err != nil { + return b, err + } + b = append(b, ")"...) + + b = append(b, " REFERENCES "...) + b = m.appendFQN(fmter, b, add.ForeignKey.To.TableName) + + b = append(b, " ("...) + if b, err = add.ForeignKey.To.Column.AppendQuery(fmter, b); err != nil { + return b, err + } + b = append(b, ")"...) + + return b, nil +} + +func (m *migrator) changeColumnType(fmter schema.Formatter, b []byte, colDef *migrate.ChangeColumnTypeOp) (_ []byte, err error) { + // alterColumn never re-assigns err, so there is no need to check for err != nil after calling it + var i int + appendAlterColumn := func() { + if i > 0 { + b = append(b, ", "...) + } + b = append(b, "ALTER COLUMN "...) + b = fmter.AppendName(b, colDef.Column) + i++ + } + + got, want := colDef.From, colDef.To + + inspector := m.db.Dialect().(sqlschema.InspectorDialect) + if !inspector.CompareType(want, got) { + appendAlterColumn() + b = append(b, " SET DATA TYPE "...) + if b, err = want.AppendQuery(fmter, b); err != nil { + return b, err + } + } + + // Column must be declared NOT NULL before identity can be added. + // Although PG can resolve the order of operations itself, we make this explicit in the query. + if want.GetIsNullable() != got.GetIsNullable() { + appendAlterColumn() + if !want.GetIsNullable() { + b = append(b, " SET NOT NULL"...) + } else { + b = append(b, " DROP NOT NULL"...) + } + } + + if want.GetIsIdentity() != got.GetIsIdentity() { + appendAlterColumn() + if !want.GetIsIdentity() { + b = append(b, " DROP IDENTITY"...) + } else { + b = append(b, " ADD"...) + b = appendGeneratedAsIdentity(b) + } + } + + if want.GetDefaultValue() != got.GetDefaultValue() { + appendAlterColumn() + if want.GetDefaultValue() == "" { + b = append(b, " DROP DEFAULT"...) + } else { + b = append(b, " SET DEFAULT "...) + b = append(b, want.GetDefaultValue()...) + } + } + + return b, nil +} diff --git a/vendor/github.com/uptrace/bun/dialect/pgdialect/array.go b/vendor/github.com/uptrace/bun/dialect/pgdialect/array.go index 46b55659b..b0d9a2331 100644 --- a/vendor/github.com/uptrace/bun/dialect/pgdialect/array.go +++ b/vendor/github.com/uptrace/bun/dialect/pgdialect/array.go @@ -5,6 +5,7 @@ import ( "database/sql/driver" "encoding/hex" "fmt" + "math" "reflect" "strconv" "time" @@ -159,7 +160,7 @@ func arrayAppend(fmter schema.Formatter, b []byte, v interface{}) []byte { case int64: return strconv.AppendInt(b, v, 10) case float64: - return dialect.AppendFloat64(b, v) + return arrayAppendFloat64(b, v) case bool: return dialect.AppendBool(b, v) case []byte: @@ -167,7 +168,10 @@ func arrayAppend(fmter schema.Formatter, b []byte, v interface{}) []byte { case string: return arrayAppendString(b, v) case time.Time: - return fmter.Dialect().AppendTime(b, v) + b = append(b, '"') + b = appendTime(b, v) + b = append(b, '"') + return b default: err := fmt.Errorf("pgdialect: can't append %T", v) return dialect.AppendError(b, err) @@ -288,7 +292,7 @@ func appendFloat64Slice(b []byte, floats []float64) []byte { b = append(b, '{') for _, n := range floats { - b = dialect.AppendFloat64(b, n) + b = arrayAppendFloat64(b, n) b = append(b, ',') } if len(floats) > 0 { @@ -302,6 +306,19 @@ func appendFloat64Slice(b []byte, floats []float64) []byte { return b } +func arrayAppendFloat64(b []byte, num float64) []byte { + switch { + case math.IsNaN(num): + return append(b, "NaN"...) + case math.IsInf(num, 1): + return append(b, "Infinity"...) + case math.IsInf(num, -1): + return append(b, "-Infinity"...) + default: + return strconv.AppendFloat(b, num, 'f', -1, 64) + } +} + func appendTimeSliceValue(fmter schema.Formatter, b []byte, v reflect.Value) []byte { ts := v.Convert(sliceTimeType).Interface().([]time.Time) return appendTimeSlice(fmter, b, ts) @@ -383,6 +400,10 @@ func arrayScanner(typ reflect.Type) schema.ScannerFunc { } } + if src == nil { + return nil + } + b, err := toBytes(src) if err != nil { return err @@ -553,7 +574,7 @@ func scanFloat64SliceValue(dest reflect.Value, src interface{}) error { } func scanFloat64Slice(src interface{}) ([]float64, error) { - if src == -1 { + if src == nil { return nil, nil } @@ -593,7 +614,7 @@ func toBytes(src interface{}) ([]byte, error) { case []byte: return src, nil default: - return nil, fmt.Errorf("bun: got %T, wanted []byte or string", src) + return nil, fmt.Errorf("pgdialect: got %T, wanted []byte or string", src) } } diff --git a/vendor/github.com/uptrace/bun/dialect/pgdialect/dialect.go b/vendor/github.com/uptrace/bun/dialect/pgdialect/dialect.go index 358971f61..040163f98 100644 --- a/vendor/github.com/uptrace/bun/dialect/pgdialect/dialect.go +++ b/vendor/github.com/uptrace/bun/dialect/pgdialect/dialect.go @@ -10,6 +10,7 @@ import ( "github.com/uptrace/bun/dialect" "github.com/uptrace/bun/dialect/feature" "github.com/uptrace/bun/dialect/sqltype" + "github.com/uptrace/bun/migrate/sqlschema" "github.com/uptrace/bun/schema" ) @@ -29,6 +30,10 @@ type Dialect struct { features feature.Feature } +var _ schema.Dialect = (*Dialect)(nil) +var _ sqlschema.InspectorDialect = (*Dialect)(nil) +var _ sqlschema.MigratorDialect = (*Dialect)(nil) + func New() *Dialect { d := new(Dialect) d.tables = schema.NewTables(d) @@ -48,7 +53,8 @@ func New() *Dialect { feature.InsertOnConflict | feature.SelectExists | feature.GeneratedIdentity | - feature.CompositeIn + feature.CompositeIn | + feature.DeleteReturning return d } @@ -118,5 +124,10 @@ func (d *Dialect) AppendUint64(b []byte, n uint64) []byte { } func (d *Dialect) AppendSequence(b []byte, _ *schema.Table, _ *schema.Field) []byte { + return appendGeneratedAsIdentity(b) +} + +// appendGeneratedAsIdentity appends GENERATED BY DEFAULT AS IDENTITY to the column definition. +func appendGeneratedAsIdentity(b []byte) []byte { return append(b, " GENERATED BY DEFAULT AS IDENTITY"...) } diff --git a/vendor/github.com/uptrace/bun/dialect/pgdialect/inspector.go b/vendor/github.com/uptrace/bun/dialect/pgdialect/inspector.go new file mode 100644 index 000000000..42bbbe84f --- /dev/null +++ b/vendor/github.com/uptrace/bun/dialect/pgdialect/inspector.go @@ -0,0 +1,297 @@ +package pgdialect + +import ( + "context" + "strings" + + "github.com/uptrace/bun" + "github.com/uptrace/bun/migrate/sqlschema" + orderedmap "github.com/wk8/go-ordered-map/v2" +) + +type ( + Schema = sqlschema.BaseDatabase + Table = sqlschema.BaseTable + Column = sqlschema.BaseColumn +) + +func (d *Dialect) NewInspector(db *bun.DB, options ...sqlschema.InspectorOption) sqlschema.Inspector { + return newInspector(db, options...) +} + +type Inspector struct { + sqlschema.InspectorConfig + db *bun.DB +} + +var _ sqlschema.Inspector = (*Inspector)(nil) + +func newInspector(db *bun.DB, options ...sqlschema.InspectorOption) *Inspector { + i := &Inspector{db: db} + sqlschema.ApplyInspectorOptions(&i.InspectorConfig, options...) + return i +} + +func (in *Inspector) Inspect(ctx context.Context) (sqlschema.Database, error) { + dbSchema := Schema{ + Tables: orderedmap.New[string, sqlschema.Table](), + ForeignKeys: make(map[sqlschema.ForeignKey]string), + } + + exclude := in.ExcludeTables + if len(exclude) == 0 { + // Avoid getting NOT IN (NULL) if bun.In() is called with an empty slice. + exclude = []string{""} + } + + var tables []*InformationSchemaTable + if err := in.db.NewRaw(sqlInspectTables, in.SchemaName, bun.In(exclude)).Scan(ctx, &tables); err != nil { + return dbSchema, err + } + + var fks []*ForeignKey + if err := in.db.NewRaw(sqlInspectForeignKeys, in.SchemaName, bun.In(exclude), bun.In(exclude)).Scan(ctx, &fks); err != nil { + return dbSchema, err + } + dbSchema.ForeignKeys = make(map[sqlschema.ForeignKey]string, len(fks)) + + for _, table := range tables { + var columns []*InformationSchemaColumn + if err := in.db.NewRaw(sqlInspectColumnsQuery, table.Schema, table.Name).Scan(ctx, &columns); err != nil { + return dbSchema, err + } + + colDefs := orderedmap.New[string, sqlschema.Column]() + uniqueGroups := make(map[string][]string) + + for _, c := range columns { + def := c.Default + if c.IsSerial || c.IsIdentity { + def = "" + } else if !c.IsDefaultLiteral { + def = strings.ToLower(def) + } + + colDefs.Set(c.Name, &Column{ + Name: c.Name, + SQLType: c.DataType, + VarcharLen: c.VarcharLen, + DefaultValue: def, + IsNullable: c.IsNullable, + IsAutoIncrement: c.IsSerial, + IsIdentity: c.IsIdentity, + }) + + for _, group := range c.UniqueGroups { + uniqueGroups[group] = append(uniqueGroups[group], c.Name) + } + } + + var unique []sqlschema.Unique + for name, columns := range uniqueGroups { + unique = append(unique, sqlschema.Unique{ + Name: name, + Columns: sqlschema.NewColumns(columns...), + }) + } + + var pk *sqlschema.PrimaryKey + if len(table.PrimaryKey.Columns) > 0 { + pk = &sqlschema.PrimaryKey{ + Name: table.PrimaryKey.ConstraintName, + Columns: sqlschema.NewColumns(table.PrimaryKey.Columns...), + } + } + + dbSchema.Tables.Set(table.Name, &Table{ + Schema: table.Schema, + Name: table.Name, + Columns: colDefs, + PrimaryKey: pk, + UniqueConstraints: unique, + }) + } + + for _, fk := range fks { + dbSchema.ForeignKeys[sqlschema.ForeignKey{ + From: sqlschema.NewColumnReference(fk.SourceTable, fk.SourceColumns...), + To: sqlschema.NewColumnReference(fk.TargetTable, fk.TargetColumns...), + }] = fk.ConstraintName + } + return dbSchema, nil +} + +type InformationSchemaTable struct { + Schema string `bun:"table_schema,pk"` + Name string `bun:"table_name,pk"` + PrimaryKey PrimaryKey `bun:"embed:primary_key_"` + + Columns []*InformationSchemaColumn `bun:"rel:has-many,join:table_schema=table_schema,join:table_name=table_name"` +} + +type InformationSchemaColumn struct { + Schema string `bun:"table_schema"` + Table string `bun:"table_name"` + Name string `bun:"column_name"` + DataType string `bun:"data_type"` + VarcharLen int `bun:"varchar_len"` + IsArray bool `bun:"is_array"` + ArrayDims int `bun:"array_dims"` + Default string `bun:"default"` + IsDefaultLiteral bool `bun:"default_is_literal_expr"` + IsIdentity bool `bun:"is_identity"` + IndentityType string `bun:"identity_type"` + IsSerial bool `bun:"is_serial"` + IsNullable bool `bun:"is_nullable"` + UniqueGroups []string `bun:"unique_groups,array"` +} + +type ForeignKey struct { + ConstraintName string `bun:"constraint_name"` + SourceSchema string `bun:"schema_name"` + SourceTable string `bun:"table_name"` + SourceColumns []string `bun:"columns,array"` + TargetSchema string `bun:"target_schema"` + TargetTable string `bun:"target_table"` + TargetColumns []string `bun:"target_columns,array"` +} + +type PrimaryKey struct { + ConstraintName string `bun:"name"` + Columns []string `bun:"columns,array"` +} + +const ( + // sqlInspectTables retrieves all user-defined tables in the selected schema. + // Pass bun.In([]string{...}) to exclude tables from this inspection or bun.In([]string{''}) to include all results. + sqlInspectTables = ` +SELECT + "t".table_schema, + "t".table_name, + pk.name AS primary_key_name, + pk.columns AS primary_key_columns +FROM information_schema.tables "t" + LEFT JOIN ( + SELECT i.indrelid, "idx".relname AS "name", ARRAY_AGG("a".attname) AS "columns" + FROM pg_index i + JOIN pg_attribute "a" + ON "a".attrelid = i.indrelid + AND "a".attnum = ANY("i".indkey) + AND i.indisprimary + JOIN pg_class "idx" ON i.indexrelid = "idx".oid + GROUP BY 1, 2 + ) pk + ON ("t".table_schema || '.' || "t".table_name)::regclass = pk.indrelid +WHERE table_type = 'BASE TABLE' + AND "t".table_schema = ? + AND "t".table_schema NOT LIKE 'pg_%' + AND "table_name" NOT IN (?) +ORDER BY "t".table_schema, "t".table_name +` + + // sqlInspectColumnsQuery retrieves column definitions for the specified table. + // Unlike sqlInspectTables and sqlInspectSchema, it should be passed to bun.NewRaw + // with additional args for table_schema and table_name. + sqlInspectColumnsQuery = ` +SELECT + "c".table_schema, + "c".table_name, + "c".column_name, + "c".data_type, + "c".character_maximum_length::integer AS varchar_len, + "c".data_type = 'ARRAY' AS is_array, + COALESCE("c".array_dims, 0) AS array_dims, + CASE + WHEN "c".column_default ~ '^''.*''::.*$' THEN substring("c".column_default FROM '^''(.*)''::.*$') + ELSE "c".column_default + END AS "default", + "c".column_default ~ '^''.*''::.*$' OR "c".column_default ~ '^[0-9\.]+$' AS default_is_literal_expr, + "c".is_identity = 'YES' AS is_identity, + "c".column_default = format('nextval(''%s_%s_seq''::regclass)', "c".table_name, "c".column_name) AS is_serial, + COALESCE("c".identity_type, '') AS identity_type, + "c".is_nullable = 'YES' AS is_nullable, + "c"."unique_groups" AS unique_groups +FROM ( + SELECT + "table_schema", + "table_name", + "column_name", + "c".data_type, + "c".character_maximum_length, + "c".column_default, + "c".is_identity, + "c".is_nullable, + att.array_dims, + att.identity_type, + att."unique_groups", + att."constraint_type" + FROM information_schema.columns "c" + LEFT JOIN ( + SELECT + s.nspname AS "table_schema", + "t".relname AS "table_name", + "c".attname AS "column_name", + "c".attndims AS array_dims, + "c".attidentity AS identity_type, + ARRAY_AGG(con.conname) FILTER (WHERE con.contype = 'u') AS "unique_groups", + ARRAY_AGG(con.contype) AS "constraint_type" + FROM ( + SELECT + conname, + contype, + connamespace, + conrelid, + conrelid AS attrelid, + UNNEST(conkey) AS attnum + FROM pg_constraint + ) con + LEFT JOIN pg_attribute "c" USING (attrelid, attnum) + LEFT JOIN pg_namespace s ON s.oid = con.connamespace + LEFT JOIN pg_class "t" ON "t".oid = con.conrelid + GROUP BY 1, 2, 3, 4, 5 + ) att USING ("table_schema", "table_name", "column_name") + ) "c" +WHERE "table_schema" = ? AND "table_name" = ? +ORDER BY "table_schema", "table_name", "column_name" +` + + // sqlInspectForeignKeys get FK definitions for user-defined tables. + // Pass bun.In([]string{...}) to exclude tables from this inspection or bun.In([]string{''}) to include all results. + sqlInspectForeignKeys = ` +WITH + "schemas" AS ( + SELECT oid, nspname + FROM pg_namespace + ), + "tables" AS ( + SELECT oid, relnamespace, relname, relkind + FROM pg_class + ), + "columns" AS ( + SELECT attrelid, attname, attnum + FROM pg_attribute + WHERE attisdropped = false + ) +SELECT DISTINCT + co.conname AS "constraint_name", + ss.nspname AS schema_name, + s.relname AS "table_name", + ARRAY_AGG(sc.attname) AS "columns", + ts.nspname AS target_schema, + "t".relname AS target_table, + ARRAY_AGG(tc.attname) AS target_columns +FROM pg_constraint co + LEFT JOIN "tables" s ON s.oid = co.conrelid + LEFT JOIN "schemas" ss ON ss.oid = s.relnamespace + LEFT JOIN "columns" sc ON sc.attrelid = s.oid AND sc.attnum = ANY(co.conkey) + LEFT JOIN "tables" t ON t.oid = co.confrelid + LEFT JOIN "schemas" ts ON ts.oid = "t".relnamespace + LEFT JOIN "columns" tc ON tc.attrelid = "t".oid AND tc.attnum = ANY(co.confkey) +WHERE co.contype = 'f' + AND co.conrelid IN (SELECT oid FROM pg_class WHERE relkind = 'r') + AND ARRAY_POSITION(co.conkey, sc.attnum) = ARRAY_POSITION(co.confkey, tc.attnum) + AND ss.nspname = ? + AND s.relname NOT IN (?) AND "t".relname NOT IN (?) +GROUP BY "constraint_name", "schema_name", "table_name", target_schema, target_table +` +) diff --git a/vendor/github.com/uptrace/bun/dialect/pgdialect/sqltype.go b/vendor/github.com/uptrace/bun/dialect/pgdialect/sqltype.go index fad84209d..bacc00e86 100644 --- a/vendor/github.com/uptrace/bun/dialect/pgdialect/sqltype.go +++ b/vendor/github.com/uptrace/bun/dialect/pgdialect/sqltype.go @@ -5,18 +5,22 @@ import ( "encoding/json" "net" "reflect" + "strings" "github.com/uptrace/bun/dialect/sqltype" + "github.com/uptrace/bun/migrate/sqlschema" "github.com/uptrace/bun/schema" ) const ( // Date / Time - pgTypeTimestampTz = "TIMESTAMPTZ" // Timestamp with a time zone - pgTypeDate = "DATE" // Date - pgTypeTime = "TIME" // Time without a time zone - pgTypeTimeTz = "TIME WITH TIME ZONE" // Time with a time zone - pgTypeInterval = "INTERVAL" // Time Interval + pgTypeTimestamp = "TIMESTAMP" // Timestamp + pgTypeTimestampWithTz = "TIMESTAMP WITH TIME ZONE" // Timestamp with a time zone + pgTypeTimestampTz = "TIMESTAMPTZ" // Timestamp with a time zone (alias) + pgTypeDate = "DATE" // Date + pgTypeTime = "TIME" // Time without a time zone + pgTypeTimeTz = "TIME WITH TIME ZONE" // Time with a time zone + pgTypeInterval = "INTERVAL" // Time interval // Network Addresses pgTypeInet = "INET" // IPv4 or IPv6 hosts and networks @@ -28,6 +32,13 @@ const ( pgTypeSerial = "SERIAL" // 4 byte autoincrementing integer pgTypeBigSerial = "BIGSERIAL" // 8 byte autoincrementing integer + // Character Types + pgTypeChar = "CHAR" // fixed length string (blank padded) + pgTypeCharacter = "CHARACTER" // alias for CHAR + pgTypeText = "TEXT" // variable length string without limit + pgTypeVarchar = "VARCHAR" // variable length string with optional limit + pgTypeCharacterVarying = "CHARACTER VARYING" // alias for VARCHAR + // Binary Data Types pgTypeBytea = "BYTEA" // binary string ) @@ -43,6 +54,10 @@ func (d *Dialect) DefaultVarcharLen() int { return 0 } +func (d *Dialect) DefaultSchema() string { + return "public" +} + func fieldSQLType(field *schema.Field) string { if field.UserSQLType != "" { return field.UserSQLType @@ -103,3 +118,62 @@ func sqlType(typ reflect.Type) string { return sqlType } + +var ( + char = newAliases(pgTypeChar, pgTypeCharacter) + varchar = newAliases(pgTypeVarchar, pgTypeCharacterVarying) + timestampTz = newAliases(sqltype.Timestamp, pgTypeTimestampTz, pgTypeTimestampWithTz) +) + +func (d *Dialect) CompareType(col1, col2 sqlschema.Column) bool { + typ1, typ2 := strings.ToUpper(col1.GetSQLType()), strings.ToUpper(col2.GetSQLType()) + + if typ1 == typ2 { + return checkVarcharLen(col1, col2, d.DefaultVarcharLen()) + } + + switch { + case char.IsAlias(typ1) && char.IsAlias(typ2): + return checkVarcharLen(col1, col2, d.DefaultVarcharLen()) + case varchar.IsAlias(typ1) && varchar.IsAlias(typ2): + return checkVarcharLen(col1, col2, d.DefaultVarcharLen()) + case timestampTz.IsAlias(typ1) && timestampTz.IsAlias(typ2): + return true + } + return false +} + +// checkVarcharLen returns true if columns have the same VarcharLen, or, +// if one specifies no VarcharLen and the other one has the default lenght for pgdialect. +// We assume that the types are otherwise equivalent and that any non-character column +// would have VarcharLen == 0; +func checkVarcharLen(col1, col2 sqlschema.Column, defaultLen int) bool { + vl1, vl2 := col1.GetVarcharLen(), col2.GetVarcharLen() + + if vl1 == vl2 { + return true + } + + if (vl1 == 0 && vl2 == defaultLen) || (vl1 == defaultLen && vl2 == 0) { + return true + } + return false +} + +// typeAlias defines aliases for common data types. It is a lightweight string set implementation. +type typeAlias map[string]struct{} + +// IsAlias checks if typ1 and typ2 are aliases of the same data type. +func (t typeAlias) IsAlias(typ string) bool { + _, ok := t[typ] + return ok +} + +// newAliases creates a set of aliases. +func newAliases(aliases ...string) typeAlias { + types := make(typeAlias) + for _, a := range aliases { + types[a] = struct{}{} + } + return types +} diff --git a/vendor/github.com/uptrace/bun/dialect/pgdialect/version.go b/vendor/github.com/uptrace/bun/dialect/pgdialect/version.go index c06043647..a4a6a760a 100644 --- a/vendor/github.com/uptrace/bun/dialect/pgdialect/version.go +++ b/vendor/github.com/uptrace/bun/dialect/pgdialect/version.go @@ -2,5 +2,5 @@ package pgdialect // Version is the current release version. func Version() string { - return "1.2.5" + return "1.2.6" } diff --git a/vendor/github.com/uptrace/bun/dialect/sqlitedialect/dialect.go b/vendor/github.com/uptrace/bun/dialect/sqlitedialect/dialect.go index 3bfe500ff..92959482e 100644 --- a/vendor/github.com/uptrace/bun/dialect/sqlitedialect/dialect.go +++ b/vendor/github.com/uptrace/bun/dialect/sqlitedialect/dialect.go @@ -40,7 +40,8 @@ func New() *Dialect { feature.TableNotExists | feature.SelectExists | feature.AutoIncrement | - feature.CompositeIn + feature.CompositeIn | + feature.DeleteReturning return d } @@ -96,9 +97,13 @@ func (d *Dialect) DefaultVarcharLen() int { // AUTOINCREMENT is only valid for INTEGER PRIMARY KEY, and this method will be a noop for other columns. // // Because this is a valid construct: +// // CREATE TABLE ("id" INTEGER PRIMARY KEY AUTOINCREMENT); +// // and this is not: +// // CREATE TABLE ("id" INTEGER AUTOINCREMENT, PRIMARY KEY ("id")); +// // AppendSequence adds a primary key constraint as a *side-effect*. Callers should expect it to avoid building invalid SQL. // SQLite also [does not support] AUTOINCREMENT column in composite primary keys. // @@ -111,6 +116,13 @@ func (d *Dialect) AppendSequence(b []byte, table *schema.Table, field *schema.Fi return b } +// DefaultSchemaName is the "schema-name" of the main database. +// The details might differ from other dialects, but for all means and purposes +// "main" is the default schema in an SQLite database. +func (d *Dialect) DefaultSchema() string { + return "main" +} + func fieldSQLType(field *schema.Field) string { switch field.DiscoveredSQLType { case sqltype.SmallInt, sqltype.BigInt: diff --git a/vendor/github.com/uptrace/bun/dialect/sqlitedialect/version.go b/vendor/github.com/uptrace/bun/dialect/sqlitedialect/version.go index e3cceaa77..18c6f7857 100644 --- a/vendor/github.com/uptrace/bun/dialect/sqlitedialect/version.go +++ b/vendor/github.com/uptrace/bun/dialect/sqlitedialect/version.go @@ -2,5 +2,5 @@ package sqlitedialect // Version is the current release version. func Version() string { - return "1.2.5" + return "1.2.6" } diff --git a/vendor/github.com/uptrace/bun/internal/util.go b/vendor/github.com/uptrace/bun/internal/util.go index 66b92b3c5..ba1341e61 100644 --- a/vendor/github.com/uptrace/bun/internal/util.go +++ b/vendor/github.com/uptrace/bun/internal/util.go @@ -79,3 +79,9 @@ func indirectNil(v reflect.Value) reflect.Value { } return v } + +// MakeQueryBytes returns zero-length byte slice with capacity of 4096. +func MakeQueryBytes() []byte { + // TODO: make this configurable? + return make([]byte, 0, 4096) +} diff --git a/vendor/github.com/uptrace/bun/migrate/auto.go b/vendor/github.com/uptrace/bun/migrate/auto.go new file mode 100644 index 000000000..e56fa23a0 --- /dev/null +++ b/vendor/github.com/uptrace/bun/migrate/auto.go @@ -0,0 +1,429 @@ +package migrate + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/uptrace/bun" + "github.com/uptrace/bun/internal" + "github.com/uptrace/bun/migrate/sqlschema" + "github.com/uptrace/bun/schema" +) + +type AutoMigratorOption func(m *AutoMigrator) + +// WithModel adds a bun.Model to the scope of migrations. +func WithModel(models ...interface{}) AutoMigratorOption { + return func(m *AutoMigrator) { + m.includeModels = append(m.includeModels, models...) + } +} + +// WithExcludeTable tells the AutoMigrator to ignore a table in the database. +// This prevents AutoMigrator from dropping tables which may exist in the schema +// but which are not used by the application. +// +// Do not exclude tables included via WithModel, as BunModelInspector ignores this setting. +func WithExcludeTable(tables ...string) AutoMigratorOption { + return func(m *AutoMigrator) { + m.excludeTables = append(m.excludeTables, tables...) + } +} + +// WithSchemaName changes the default database schema to migrate objects in. +func WithSchemaName(schemaName string) AutoMigratorOption { + return func(m *AutoMigrator) { + m.schemaName = schemaName + } +} + +// WithTableNameAuto overrides default migrations table name. +func WithTableNameAuto(table string) AutoMigratorOption { + return func(m *AutoMigrator) { + m.table = table + m.migratorOpts = append(m.migratorOpts, WithTableName(table)) + } +} + +// WithLocksTableNameAuto overrides default migration locks table name. +func WithLocksTableNameAuto(table string) AutoMigratorOption { + return func(m *AutoMigrator) { + m.locksTable = table + m.migratorOpts = append(m.migratorOpts, WithLocksTableName(table)) + } +} + +// WithMarkAppliedOnSuccessAuto sets the migrator to only mark migrations as applied/unapplied +// when their up/down is successful. +func WithMarkAppliedOnSuccessAuto(enabled bool) AutoMigratorOption { + return func(m *AutoMigrator) { + m.migratorOpts = append(m.migratorOpts, WithMarkAppliedOnSuccess(enabled)) + } +} + +// WithMigrationsDirectoryAuto overrides the default directory for migration files. +func WithMigrationsDirectoryAuto(directory string) AutoMigratorOption { + return func(m *AutoMigrator) { + m.migrationsOpts = append(m.migrationsOpts, WithMigrationsDirectory(directory)) + } +} + +// AutoMigrator performs automated schema migrations. +// +// It is designed to be a drop-in replacement for some Migrator functionality and supports all existing +// configuration options. +// Similarly to Migrator, it has methods to create SQL migrations, write them to a file, and apply them. +// Unlike Migrator, it detects the differences between the state defined by bun models and the current +// database schema automatically. +// +// Usage: +// 1. Generate migrations and apply them au once with AutoMigrator.Migrate(). +// 2. Create up- and down-SQL migration files and apply migrations using Migrator.Migrate(). +// +// While both methods produce complete, reversible migrations (with entries in the database +// and SQL migration files), prefer creating migrations and applying them separately for +// any non-trivial cases to ensure AutoMigrator detects expected changes correctly. +// +// Limitations: +// - AutoMigrator only supports a subset of the possible ALTER TABLE modifications. +// - Some changes are not automatically reversible. For example, you would need to manually +// add a CREATE TABLE query to the .down migration file to revert a DROP TABLE migration. +// - Does not validate most dialect-specific constraints. For example, when changing column +// data type, make sure the data con be auto-casted to the new type. +// - Due to how the schema-state diff is calculated, it is not possible to rename a table and +// modify any of its columns' _data type_ in a single run. This will cause the AutoMigrator +// to drop and re-create the table under a different name; it is better to apply this change in 2 steps. +// Renaming a table and renaming its columns at the same time is possible. +// - Renaming table/column to an existing name, i.e. like this [A->B] [B->C], is not possible due to how +// AutoMigrator distinguishes "rename" and "unchanged" columns. +// +// Dialect must implement both sqlschema.Inspector and sqlschema.Migrator to be used with AutoMigrator. +type AutoMigrator struct { + db *bun.DB + + // dbInspector creates the current state for the target database. + dbInspector sqlschema.Inspector + + // modelInspector creates the desired state based on the model definitions. + modelInspector sqlschema.Inspector + + // dbMigrator executes ALTER TABLE queries. + dbMigrator sqlschema.Migrator + + table string // Migrations table (excluded from database inspection) + locksTable string // Migration locks table (excluded from database inspection) + + // schemaName is the database schema considered for migration. + schemaName string + + // includeModels define the migration scope. + includeModels []interface{} + + // excludeTables are excluded from database inspection. + excludeTables []string + + // diffOpts are passed to detector constructor. + diffOpts []diffOption + + // migratorOpts are passed to Migrator constructor. + migratorOpts []MigratorOption + + // migrationsOpts are passed to Migrations constructor. + migrationsOpts []MigrationsOption +} + +func NewAutoMigrator(db *bun.DB, opts ...AutoMigratorOption) (*AutoMigrator, error) { + am := &AutoMigrator{ + db: db, + table: defaultTable, + locksTable: defaultLocksTable, + schemaName: db.Dialect().DefaultSchema(), + } + + for _, opt := range opts { + opt(am) + } + am.excludeTables = append(am.excludeTables, am.table, am.locksTable) + + dbInspector, err := sqlschema.NewInspector(db, sqlschema.WithSchemaName(am.schemaName), sqlschema.WithExcludeTables(am.excludeTables...)) + if err != nil { + return nil, err + } + am.dbInspector = dbInspector + am.diffOpts = append(am.diffOpts, withCompareTypeFunc(db.Dialect().(sqlschema.InspectorDialect).CompareType)) + + dbMigrator, err := sqlschema.NewMigrator(db, am.schemaName) + if err != nil { + return nil, err + } + am.dbMigrator = dbMigrator + + tables := schema.NewTables(db.Dialect()) + tables.Register(am.includeModels...) + am.modelInspector = sqlschema.NewBunModelInspector(tables, sqlschema.WithSchemaName(am.schemaName)) + + return am, nil +} + +func (am *AutoMigrator) plan(ctx context.Context) (*changeset, error) { + var err error + + got, err := am.dbInspector.Inspect(ctx) + if err != nil { + return nil, err + } + + want, err := am.modelInspector.Inspect(ctx) + if err != nil { + return nil, err + } + + changes := diff(got, want, am.diffOpts...) + if err := changes.ResolveDependencies(); err != nil { + return nil, fmt.Errorf("plan migrations: %w", err) + } + return changes, nil +} + +// Migrate writes required changes to a new migration file and runs the migration. +// This will create and entry in the migrations table, making it possible to revert +// the changes with Migrator.Rollback(). MigrationOptions are passed on to Migrator.Migrate(). +func (am *AutoMigrator) Migrate(ctx context.Context, opts ...MigrationOption) (*MigrationGroup, error) { + migrations, _, err := am.createSQLMigrations(ctx, false) + if err != nil { + return nil, fmt.Errorf("auto migrate: %w", err) + } + + migrator := NewMigrator(am.db, migrations, am.migratorOpts...) + if err := migrator.Init(ctx); err != nil { + return nil, fmt.Errorf("auto migrate: %w", err) + } + + group, err := migrator.Migrate(ctx, opts...) + if err != nil { + return nil, fmt.Errorf("auto migrate: %w", err) + } + return group, nil +} + +// CreateSQLMigration writes required changes to a new migration file. +// Use migrate.Migrator to apply the generated migrations. +func (am *AutoMigrator) CreateSQLMigrations(ctx context.Context) ([]*MigrationFile, error) { + _, files, err := am.createSQLMigrations(ctx, true) + return files, err +} + +// CreateTxSQLMigration writes required changes to a new migration file making sure they will be executed +// in a transaction when applied. Use migrate.Migrator to apply the generated migrations. +func (am *AutoMigrator) CreateTxSQLMigrations(ctx context.Context) ([]*MigrationFile, error) { + _, files, err := am.createSQLMigrations(ctx, false) + return files, err +} + +func (am *AutoMigrator) createSQLMigrations(ctx context.Context, transactional bool) (*Migrations, []*MigrationFile, error) { + changes, err := am.plan(ctx) + if err != nil { + return nil, nil, fmt.Errorf("create sql migrations: %w", err) + } + + name, _ := genMigrationName(am.schemaName + "_auto") + migrations := NewMigrations(am.migrationsOpts...) + migrations.Add(Migration{ + Name: name, + Up: changes.Up(am.dbMigrator), + Down: changes.Down(am.dbMigrator), + Comment: "Changes detected by bun.AutoMigrator", + }) + + // Append .tx.up.sql or .up.sql to migration name, dependin if it should be transactional. + fname := func(direction string) string { + return name + map[bool]string{true: ".tx.", false: "."}[transactional] + direction + ".sql" + } + + up, err := am.createSQL(ctx, migrations, fname("up"), changes, transactional) + if err != nil { + return nil, nil, fmt.Errorf("create sql migration up: %w", err) + } + + down, err := am.createSQL(ctx, migrations, fname("down"), changes.GetReverse(), transactional) + if err != nil { + return nil, nil, fmt.Errorf("create sql migration down: %w", err) + } + return migrations, []*MigrationFile{up, down}, nil +} + +func (am *AutoMigrator) createSQL(_ context.Context, migrations *Migrations, fname string, changes *changeset, transactional bool) (*MigrationFile, error) { + var buf bytes.Buffer + + if transactional { + buf.WriteString("SET statement_timeout = 0;") + } + + if err := changes.WriteTo(&buf, am.dbMigrator); err != nil { + return nil, err + } + content := buf.Bytes() + + fpath := filepath.Join(migrations.getDirectory(), fname) + if err := os.WriteFile(fpath, content, 0o644); err != nil { + return nil, err + } + + mf := &MigrationFile{ + Name: fname, + Path: fpath, + Content: string(content), + } + return mf, nil +} + +// Func creates a MigrationFunc that applies all operations all the changeset. +func (c *changeset) Func(m sqlschema.Migrator) MigrationFunc { + return func(ctx context.Context, db *bun.DB) error { + return c.apply(ctx, db, m) + } +} + +// GetReverse returns a new changeset with each operation in it "reversed" and in reverse order. +func (c *changeset) GetReverse() *changeset { + var reverse changeset + for i := len(c.operations) - 1; i >= 0; i-- { + reverse.Add(c.operations[i].GetReverse()) + } + return &reverse +} + +// Up is syntactic sugar. +func (c *changeset) Up(m sqlschema.Migrator) MigrationFunc { + return c.Func(m) +} + +// Down is syntactic sugar. +func (c *changeset) Down(m sqlschema.Migrator) MigrationFunc { + return c.GetReverse().Func(m) +} + +// apply generates SQL for each operation and executes it. +func (c *changeset) apply(ctx context.Context, db *bun.DB, m sqlschema.Migrator) error { + if len(c.operations) == 0 { + return nil + } + + for _, op := range c.operations { + if _, isComment := op.(*comment); isComment { + continue + } + + b := internal.MakeQueryBytes() + b, err := m.AppendSQL(b, op) + if err != nil { + return fmt.Errorf("apply changes: %w", err) + } + + query := internal.String(b) + if _, err = db.ExecContext(ctx, query); err != nil { + return fmt.Errorf("apply changes: %w", err) + } + } + return nil +} + +func (c *changeset) WriteTo(w io.Writer, m sqlschema.Migrator) error { + var err error + + b := internal.MakeQueryBytes() + for _, op := range c.operations { + if c, isComment := op.(*comment); isComment { + b = append(b, "/*\n"...) + b = append(b, *c...) + b = append(b, "\n*/"...) + continue + } + + b, err = m.AppendSQL(b, op) + if err != nil { + return fmt.Errorf("write changeset: %w", err) + } + b = append(b, ";\n"...) + } + if _, err := w.Write(b); err != nil { + return fmt.Errorf("write changeset: %w", err) + } + return nil +} + +func (c *changeset) ResolveDependencies() error { + if len(c.operations) <= 1 { + return nil + } + + const ( + unvisited = iota + current + visited + ) + + status := make(map[Operation]int, len(c.operations)) + for _, op := range c.operations { + status[op] = unvisited + } + + var resolved []Operation + var nextOp Operation + var visit func(op Operation) error + + next := func() bool { + for op, s := range status { + if s == unvisited { + nextOp = op + return true + } + } + return false + } + + // visit iterates over c.operations until it finds all operations that depend on the current one + // or runs into cirtular dependency, in which case it will return an error. + visit = func(op Operation) error { + switch status[op] { + case visited: + return nil + case current: + // TODO: add details (circle) to the error message + return errors.New("detected circular dependency") + } + + status[op] = current + + for _, another := range c.operations { + if dop, hasDeps := another.(interface { + DependsOn(Operation) bool + }); another == op || !hasDeps || !dop.DependsOn(op) { + continue + } + if err := visit(another); err != nil { + return err + } + } + + status[op] = visited + + // Any dependent nodes would've already been added to the list by now, so we prepend. + resolved = append([]Operation{op}, resolved...) + return nil + } + + for next() { + if err := visit(nextOp); err != nil { + return err + } + } + + c.operations = resolved + return nil +} diff --git a/vendor/github.com/uptrace/bun/migrate/diff.go b/vendor/github.com/uptrace/bun/migrate/diff.go new file mode 100644 index 000000000..42e55dcde --- /dev/null +++ b/vendor/github.com/uptrace/bun/migrate/diff.go @@ -0,0 +1,411 @@ +package migrate + +import ( + "github.com/uptrace/bun/migrate/sqlschema" +) + +// changeset is a set of changes to the database schema definition. +type changeset struct { + operations []Operation +} + +// Add new operations to the changeset. +func (c *changeset) Add(op ...Operation) { + c.operations = append(c.operations, op...) +} + +// diff calculates the diff between the current database schema and the target state. +// The changeset is not sorted -- the caller should resolve dependencies before applying the changes. +func diff(got, want sqlschema.Database, opts ...diffOption) *changeset { + d := newDetector(got, want, opts...) + return d.detectChanges() +} + +func (d *detector) detectChanges() *changeset { + currentTables := d.current.GetTables() + targetTables := d.target.GetTables() + +RenameCreate: + for wantName, wantTable := range targetTables.FromOldest() { + + // A table with this name exists in the database. We assume that schema objects won't + // be renamed to an already existing name, nor do we support such cases. + // Simply check if the table definition has changed. + if haveTable, ok := currentTables.Get(wantName); ok { + d.detectColumnChanges(haveTable, wantTable, true) + d.detectConstraintChanges(haveTable, wantTable) + continue + } + + // Find all renamed tables. We assume that renamed tables have the same signature. + for haveName, haveTable := range currentTables.FromOldest() { + if _, exists := targetTables.Get(haveName); !exists && d.canRename(haveTable, wantTable) { + d.changes.Add(&RenameTableOp{ + TableName: haveTable.GetName(), + NewName: wantName, + }) + d.refMap.RenameTable(haveTable.GetName(), wantName) + + // Find renamed columns, if any, and check if constraints (PK, UNIQUE) have been updated. + // We need not check wantTable any further. + d.detectColumnChanges(haveTable, wantTable, false) + d.detectConstraintChanges(haveTable, wantTable) + currentTables.Delete(haveName) + continue RenameCreate + } + } + + // If wantTable does not exist in the database and was not renamed + // then we need to create this table in the database. + additional := wantTable.(*sqlschema.BunTable) + d.changes.Add(&CreateTableOp{ + TableName: wantTable.GetName(), + Model: additional.Model, + }) + } + + // Drop any remaining "current" tables which do not have a model. + for name, table := range currentTables.FromOldest() { + if _, keep := targetTables.Get(name); !keep { + d.changes.Add(&DropTableOp{ + TableName: table.GetName(), + }) + } + } + + targetFKs := d.target.GetForeignKeys() + currentFKs := d.refMap.Deref() + + for fk := range targetFKs { + if _, ok := currentFKs[fk]; !ok { + d.changes.Add(&AddForeignKeyOp{ + ForeignKey: fk, + ConstraintName: "", // leave empty to let each dialect apply their convention + }) + } + } + + for fk, name := range currentFKs { + if _, ok := targetFKs[fk]; !ok { + d.changes.Add(&DropForeignKeyOp{ + ConstraintName: name, + ForeignKey: fk, + }) + } + } + + return &d.changes +} + +// detechColumnChanges finds renamed columns and, if checkType == true, columns with changed type. +func (d *detector) detectColumnChanges(current, target sqlschema.Table, checkType bool) { + currentColumns := current.GetColumns() + targetColumns := target.GetColumns() + +ChangeRename: + for tName, tCol := range targetColumns.FromOldest() { + + // This column exists in the database, so it hasn't been renamed, dropped, or added. + // Still, we should not delete(columns, thisColumn), because later we will need to + // check that we do not try to rename a column to an already a name that already exists. + if cCol, ok := currentColumns.Get(tName); ok { + if checkType && !d.equalColumns(cCol, tCol) { + d.changes.Add(&ChangeColumnTypeOp{ + TableName: target.GetName(), + Column: tName, + From: cCol, + To: d.makeTargetColDef(cCol, tCol), + }) + } + continue + } + + // Column tName does not exist in the database -- it's been either renamed or added. + // Find renamed columns first. + for cName, cCol := range currentColumns.FromOldest() { + // Cannot rename if a column with this name already exists or the types differ. + if _, exists := targetColumns.Get(cName); exists || !d.equalColumns(tCol, cCol) { + continue + } + d.changes.Add(&RenameColumnOp{ + TableName: target.GetName(), + OldName: cName, + NewName: tName, + }) + d.refMap.RenameColumn(target.GetName(), cName, tName) + currentColumns.Delete(cName) // no need to check this column again + + // Update primary key definition to avoid superficially recreating the constraint. + current.GetPrimaryKey().Columns.Replace(cName, tName) + + continue ChangeRename + } + + d.changes.Add(&AddColumnOp{ + TableName: target.GetName(), + ColumnName: tName, + Column: tCol, + }) + } + + // Drop columns which do not exist in the target schema and were not renamed. + for cName, cCol := range currentColumns.FromOldest() { + if _, keep := targetColumns.Get(cName); !keep { + d.changes.Add(&DropColumnOp{ + TableName: target.GetName(), + ColumnName: cName, + Column: cCol, + }) + } + } +} + +func (d *detector) detectConstraintChanges(current, target sqlschema.Table) { +Add: + for _, want := range target.GetUniqueConstraints() { + for _, got := range current.GetUniqueConstraints() { + if got.Equals(want) { + continue Add + } + } + d.changes.Add(&AddUniqueConstraintOp{ + TableName: target.GetName(), + Unique: want, + }) + } + +Drop: + for _, got := range current.GetUniqueConstraints() { + for _, want := range target.GetUniqueConstraints() { + if got.Equals(want) { + continue Drop + } + } + + d.changes.Add(&DropUniqueConstraintOp{ + TableName: target.GetName(), + Unique: got, + }) + } + + targetPK := target.GetPrimaryKey() + currentPK := current.GetPrimaryKey() + + // Detect primary key changes + if targetPK == nil && currentPK == nil { + return + } + switch { + case targetPK == nil && currentPK != nil: + d.changes.Add(&DropPrimaryKeyOp{ + TableName: target.GetName(), + PrimaryKey: *currentPK, + }) + case currentPK == nil && targetPK != nil: + d.changes.Add(&AddPrimaryKeyOp{ + TableName: target.GetName(), + PrimaryKey: *targetPK, + }) + case targetPK.Columns != currentPK.Columns: + d.changes.Add(&ChangePrimaryKeyOp{ + TableName: target.GetName(), + Old: *currentPK, + New: *targetPK, + }) + } +} + +func newDetector(got, want sqlschema.Database, opts ...diffOption) *detector { + cfg := &detectorConfig{ + cmpType: func(c1, c2 sqlschema.Column) bool { + return c1.GetSQLType() == c2.GetSQLType() && c1.GetVarcharLen() == c2.GetVarcharLen() + }, + } + for _, opt := range opts { + opt(cfg) + } + + return &detector{ + current: got, + target: want, + refMap: newRefMap(got.GetForeignKeys()), + cmpType: cfg.cmpType, + } +} + +type diffOption func(*detectorConfig) + +func withCompareTypeFunc(f CompareTypeFunc) diffOption { + return func(cfg *detectorConfig) { + cfg.cmpType = f + } +} + +// detectorConfig controls how differences in the model states are resolved. +type detectorConfig struct { + cmpType CompareTypeFunc +} + +// detector may modify the passed database schemas, so it isn't safe to re-use them. +type detector struct { + // current state represents the existing database schema. + current sqlschema.Database + + // target state represents the database schema defined in bun models. + target sqlschema.Database + + changes changeset + refMap refMap + + // cmpType determines column type equivalence. + // Default is direct comparison with '==' operator, which is inaccurate + // due to the existence of dialect-specific type aliases. The caller + // should pass a concrete InspectorDialect.EquuivalentType for robust comparison. + cmpType CompareTypeFunc +} + +// canRename checks if t1 can be renamed to t2. +func (d detector) canRename(t1, t2 sqlschema.Table) bool { + return t1.GetSchema() == t2.GetSchema() && equalSignatures(t1, t2, d.equalColumns) +} + +func (d detector) equalColumns(col1, col2 sqlschema.Column) bool { + return d.cmpType(col1, col2) && + col1.GetDefaultValue() == col2.GetDefaultValue() && + col1.GetIsNullable() == col2.GetIsNullable() && + col1.GetIsAutoIncrement() == col2.GetIsAutoIncrement() && + col1.GetIsIdentity() == col2.GetIsIdentity() +} + +func (d detector) makeTargetColDef(current, target sqlschema.Column) sqlschema.Column { + // Avoid unneccessary type-change migrations if the types are equivalent. + if d.cmpType(current, target) { + target = &sqlschema.BaseColumn{ + Name: target.GetName(), + DefaultValue: target.GetDefaultValue(), + IsNullable: target.GetIsNullable(), + IsAutoIncrement: target.GetIsAutoIncrement(), + IsIdentity: target.GetIsIdentity(), + + SQLType: current.GetSQLType(), + VarcharLen: current.GetVarcharLen(), + } + } + return target +} + +type CompareTypeFunc func(sqlschema.Column, sqlschema.Column) bool + +// equalSignatures determines if two tables have the same "signature". +func equalSignatures(t1, t2 sqlschema.Table, eq CompareTypeFunc) bool { + sig1 := newSignature(t1, eq) + sig2 := newSignature(t2, eq) + return sig1.Equals(sig2) +} + +// signature is a set of column definitions, which allows "relation/name-agnostic" comparison between them; +// meaning that two columns are considered equal if their types are the same. +type signature struct { + + // underlying stores the number of occurences for each unique column type. + // It helps to account for the fact that a table might have multiple columns that have the same type. + underlying map[sqlschema.BaseColumn]int + + eq CompareTypeFunc +} + +func newSignature(t sqlschema.Table, eq CompareTypeFunc) signature { + s := signature{ + underlying: make(map[sqlschema.BaseColumn]int), + eq: eq, + } + s.scan(t) + return s +} + +// scan iterates over table's field and counts occurrences of each unique column definition. +func (s *signature) scan(t sqlschema.Table) { + for _, icol := range t.GetColumns().FromOldest() { + scanCol := icol.(*sqlschema.BaseColumn) + // This is slightly more expensive than if the columns could be compared directly + // and we always did s.underlying[col]++, but we get type-equivalence in return. + col, count := s.getCount(*scanCol) + if count == 0 { + s.underlying[*scanCol] = 1 + } else { + s.underlying[col]++ + } + } +} + +// getCount uses CompareTypeFunc to find a column with the same (equivalent) SQL type +// and returns its count. Count 0 means there are no columns with of this type. +func (s *signature) getCount(keyCol sqlschema.BaseColumn) (key sqlschema.BaseColumn, count int) { + for col, cnt := range s.underlying { + if s.eq(&col, &keyCol) { + return col, cnt + } + } + return keyCol, 0 +} + +// Equals returns true if 2 signatures share an identical set of columns. +func (s *signature) Equals(other signature) bool { + if len(s.underlying) != len(other.underlying) { + return false + } + for col, count := range s.underlying { + if _, countOther := other.getCount(col); countOther != count { + return false + } + } + return true +} + +// refMap is a utility for tracking superficial changes in foreign keys, +// which do not require any modificiation in the database. +// Modern SQL dialects automatically updated foreign key constraints whenever +// a column or a table is renamed. Detector can use refMap to ignore any +// differences in foreign keys which were caused by renamed column/table. +type refMap map[*sqlschema.ForeignKey]string + +func newRefMap(fks map[sqlschema.ForeignKey]string) refMap { + rm := make(map[*sqlschema.ForeignKey]string) + for fk, name := range fks { + rm[&fk] = name + } + return rm +} + +// RenameT updates table name in all foreign key definions which depend on it. +func (rm refMap) RenameTable(tableName string, newName string) { + for fk := range rm { + switch tableName { + case fk.From.TableName: + fk.From.TableName = newName + case fk.To.TableName: + fk.To.TableName = newName + } + } +} + +// RenameColumn updates column name in all foreign key definions which depend on it. +func (rm refMap) RenameColumn(tableName string, column, newName string) { + for fk := range rm { + if tableName == fk.From.TableName { + fk.From.Column.Replace(column, newName) + } + if tableName == fk.To.TableName { + fk.To.Column.Replace(column, newName) + } + } +} + +// Deref returns copies of ForeignKey values to a map. +func (rm refMap) Deref() map[sqlschema.ForeignKey]string { + out := make(map[sqlschema.ForeignKey]string) + for fk, name := range rm { + out[*fk] = name + } + return out +} diff --git a/vendor/github.com/uptrace/bun/migrate/migrator.go b/vendor/github.com/uptrace/bun/migrate/migrator.go index e6d70e39f..d5a72aec0 100644 --- a/vendor/github.com/uptrace/bun/migrate/migrator.go +++ b/vendor/github.com/uptrace/bun/migrate/migrator.go @@ -12,14 +12,21 @@ import ( "github.com/uptrace/bun" ) +const ( + defaultTable = "bun_migrations" + defaultLocksTable = "bun_migration_locks" +) + type MigratorOption func(m *Migrator) +// WithTableName overrides default migrations table name. func WithTableName(table string) MigratorOption { return func(m *Migrator) { m.table = table } } +// WithLocksTableName overrides default migration locks table name. func WithLocksTableName(table string) MigratorOption { return func(m *Migrator) { m.locksTable = table @@ -27,7 +34,7 @@ func WithLocksTableName(table string) MigratorOption { } // WithMarkAppliedOnSuccess sets the migrator to only mark migrations as applied/unapplied -// when their up/down is successful +// when their up/down is successful. func WithMarkAppliedOnSuccess(enabled bool) MigratorOption { return func(m *Migrator) { m.markAppliedOnSuccess = enabled @@ -52,8 +59,8 @@ func NewMigrator(db *bun.DB, migrations *Migrations, opts ...MigratorOption) *Mi ms: migrations.ms, - table: "bun_migrations", - locksTable: "bun_migration_locks", + table: defaultTable, + locksTable: defaultLocksTable, } for _, opt := range opts { opt(m) @@ -246,7 +253,7 @@ func (m *Migrator) CreateGoMigration( opt(cfg) } - name, err := m.genMigrationName(name) + name, err := genMigrationName(name) if err != nil { return nil, err } @@ -269,7 +276,7 @@ func (m *Migrator) CreateGoMigration( // CreateTxSQLMigration creates transactional up and down SQL migration files. func (m *Migrator) CreateTxSQLMigrations(ctx context.Context, name string) ([]*MigrationFile, error) { - name, err := m.genMigrationName(name) + name, err := genMigrationName(name) if err != nil { return nil, err } @@ -289,7 +296,7 @@ func (m *Migrator) CreateTxSQLMigrations(ctx context.Context, name string) ([]*M // CreateSQLMigrations creates up and down SQL migration files. func (m *Migrator) CreateSQLMigrations(ctx context.Context, name string) ([]*MigrationFile, error) { - name, err := m.genMigrationName(name) + name, err := genMigrationName(name) if err != nil { return nil, err } @@ -307,7 +314,7 @@ func (m *Migrator) CreateSQLMigrations(ctx context.Context, name string) ([]*Mig return []*MigrationFile{up, down}, nil } -func (m *Migrator) createSQL(ctx context.Context, fname string, transactional bool) (*MigrationFile, error) { +func (m *Migrator) createSQL(_ context.Context, fname string, transactional bool) (*MigrationFile, error) { fpath := filepath.Join(m.migrations.getDirectory(), fname) template := sqlTemplate @@ -329,7 +336,7 @@ func (m *Migrator) createSQL(ctx context.Context, fname string, transactional bo var nameRE = regexp.MustCompile(`^[0-9a-z_\-]+$`) -func (m *Migrator) genMigrationName(name string) (string, error) { +func genMigrationName(name string) (string, error) { const timeFormat = "20060102150405" if name == "" { diff --git a/vendor/github.com/uptrace/bun/migrate/operations.go b/vendor/github.com/uptrace/bun/migrate/operations.go new file mode 100644 index 000000000..7b749c5a0 --- /dev/null +++ b/vendor/github.com/uptrace/bun/migrate/operations.go @@ -0,0 +1,340 @@ +package migrate + +import ( + "fmt" + + "github.com/uptrace/bun/migrate/sqlschema" +) + +// Operation encapsulates the request to change a database definition +// and knowns which operation can revert it. +// +// It is useful to define "monolith" Operations whenever possible, +// even though they a dialect may require several distinct steps to apply them. +// For example, changing a primary key involves first dropping the old constraint +// before generating the new one. Yet, this is only an implementation detail and +// passing a higher-level ChangePrimaryKeyOp will give the dialect more information +// about the applied change. +// +// Some operations might be irreversible due to technical limitations. Returning +// a *comment from GetReverse() will add an explanatory note to the generate migation file. +// +// To declare dependency on another Operation, operations should implement +// { DependsOn(Operation) bool } interface, which Changeset will use to resolve dependencies. +type Operation interface { + GetReverse() Operation +} + +// CreateTableOp creates a new table in the schema. +// +// It does not report dependency on any other migration and may be executed first. +// Make sure the dialect does not include FOREIGN KEY constraints in the CREATE TABLE +// statement, as those may potentially reference not-yet-existing columns/tables. +type CreateTableOp struct { + TableName string + Model interface{} +} + +var _ Operation = (*CreateTableOp)(nil) + +func (op *CreateTableOp) GetReverse() Operation { + return &DropTableOp{TableName: op.TableName} +} + +// DropTableOp drops a database table. This operation is not reversible. +type DropTableOp struct { + TableName string +} + +var _ Operation = (*DropTableOp)(nil) + +func (op *DropTableOp) DependsOn(another Operation) bool { + drop, ok := another.(*DropForeignKeyOp) + return ok && drop.ForeignKey.DependsOnTable(op.TableName) +} + +// GetReverse for a DropTable returns a no-op migration. Logically, CreateTable is the reverse, +// but DropTable does not have the table's definition to create one. +func (op *DropTableOp) GetReverse() Operation { + c := comment(fmt.Sprintf("WARNING: \"DROP TABLE %s\" cannot be reversed automatically because table definition is not available", op.TableName)) + return &c +} + +// RenameTableOp renames the table. Changing the "schema" part of the table's FQN (moving tables between schemas) is not allowed. +type RenameTableOp struct { + TableName string + NewName string +} + +var _ Operation = (*RenameTableOp)(nil) + +func (op *RenameTableOp) GetReverse() Operation { + return &RenameTableOp{ + TableName: op.NewName, + NewName: op.TableName, + } +} + +// RenameColumnOp renames a column in the table. If the changeset includes a rename operation +// for the column's table, it should be executed first. +type RenameColumnOp struct { + TableName string + OldName string + NewName string +} + +var _ Operation = (*RenameColumnOp)(nil) + +func (op *RenameColumnOp) GetReverse() Operation { + return &RenameColumnOp{ + TableName: op.TableName, + OldName: op.NewName, + NewName: op.OldName, + } +} + +func (op *RenameColumnOp) DependsOn(another Operation) bool { + rename, ok := another.(*RenameTableOp) + return ok && op.TableName == rename.NewName +} + +// AddColumnOp adds a new column to the table. +type AddColumnOp struct { + TableName string + ColumnName string + Column sqlschema.Column +} + +var _ Operation = (*AddColumnOp)(nil) + +func (op *AddColumnOp) GetReverse() Operation { + return &DropColumnOp{ + TableName: op.TableName, + ColumnName: op.ColumnName, + Column: op.Column, + } +} + +// DropColumnOp drop a column from the table. +// +// While some dialects allow DROP CASCADE to drop dependent constraints, +// explicit handling on constraints is preferred for transparency and debugging. +// DropColumnOp depends on DropForeignKeyOp, DropPrimaryKeyOp, and ChangePrimaryKeyOp +// if any of the constraints is defined on this table. +type DropColumnOp struct { + TableName string + ColumnName string + Column sqlschema.Column +} + +var _ Operation = (*DropColumnOp)(nil) + +func (op *DropColumnOp) GetReverse() Operation { + return &AddColumnOp{ + TableName: op.TableName, + ColumnName: op.ColumnName, + Column: op.Column, + } +} + +func (op *DropColumnOp) DependsOn(another Operation) bool { + switch drop := another.(type) { + case *DropForeignKeyOp: + return drop.ForeignKey.DependsOnColumn(op.TableName, op.ColumnName) + case *DropPrimaryKeyOp: + return op.TableName == drop.TableName && drop.PrimaryKey.Columns.Contains(op.ColumnName) + case *ChangePrimaryKeyOp: + return op.TableName == drop.TableName && drop.Old.Columns.Contains(op.ColumnName) + } + return false +} + +// AddForeignKey adds a new FOREIGN KEY constraint. +type AddForeignKeyOp struct { + ForeignKey sqlschema.ForeignKey + ConstraintName string +} + +var _ Operation = (*AddForeignKeyOp)(nil) + +func (op *AddForeignKeyOp) TableName() string { + return op.ForeignKey.From.TableName +} + +func (op *AddForeignKeyOp) DependsOn(another Operation) bool { + switch another := another.(type) { + case *RenameTableOp: + return op.ForeignKey.DependsOnTable(another.TableName) || op.ForeignKey.DependsOnTable(another.NewName) + case *CreateTableOp: + return op.ForeignKey.DependsOnTable(another.TableName) + } + return false +} + +func (op *AddForeignKeyOp) GetReverse() Operation { + return &DropForeignKeyOp{ + ForeignKey: op.ForeignKey, + ConstraintName: op.ConstraintName, + } +} + +// DropForeignKeyOp drops a FOREIGN KEY constraint. +type DropForeignKeyOp struct { + ForeignKey sqlschema.ForeignKey + ConstraintName string +} + +var _ Operation = (*DropForeignKeyOp)(nil) + +func (op *DropForeignKeyOp) TableName() string { + return op.ForeignKey.From.TableName +} + +func (op *DropForeignKeyOp) GetReverse() Operation { + return &AddForeignKeyOp{ + ForeignKey: op.ForeignKey, + ConstraintName: op.ConstraintName, + } +} + +// AddUniqueConstraintOp adds new UNIQUE constraint to the table. +type AddUniqueConstraintOp struct { + TableName string + Unique sqlschema.Unique +} + +var _ Operation = (*AddUniqueConstraintOp)(nil) + +func (op *AddUniqueConstraintOp) GetReverse() Operation { + return &DropUniqueConstraintOp{ + TableName: op.TableName, + Unique: op.Unique, + } +} + +func (op *AddUniqueConstraintOp) DependsOn(another Operation) bool { + switch another := another.(type) { + case *AddColumnOp: + return op.TableName == another.TableName && op.Unique.Columns.Contains(another.ColumnName) + case *RenameTableOp: + return op.TableName == another.NewName + case *DropUniqueConstraintOp: + // We want to drop the constraint with the same name before adding this one. + return op.TableName == another.TableName && op.Unique.Name == another.Unique.Name + default: + return false + } + +} + +// DropUniqueConstraintOp drops a UNIQUE constraint. +type DropUniqueConstraintOp struct { + TableName string + Unique sqlschema.Unique +} + +var _ Operation = (*DropUniqueConstraintOp)(nil) + +func (op *DropUniqueConstraintOp) DependsOn(another Operation) bool { + if rename, ok := another.(*RenameTableOp); ok { + return op.TableName == rename.NewName + } + return false +} + +func (op *DropUniqueConstraintOp) GetReverse() Operation { + return &AddUniqueConstraintOp{ + TableName: op.TableName, + Unique: op.Unique, + } +} + +// ChangeColumnTypeOp set a new data type for the column. +// The two types should be such that the data can be auto-casted from one to another. +// E.g. reducing VARCHAR lenght is not possible in most dialects. +// AutoMigrator does not enforce or validate these rules. +type ChangeColumnTypeOp struct { + TableName string + Column string + From sqlschema.Column + To sqlschema.Column +} + +var _ Operation = (*ChangeColumnTypeOp)(nil) + +func (op *ChangeColumnTypeOp) GetReverse() Operation { + return &ChangeColumnTypeOp{ + TableName: op.TableName, + Column: op.Column, + From: op.To, + To: op.From, + } +} + +// DropPrimaryKeyOp drops the table's PRIMARY KEY. +type DropPrimaryKeyOp struct { + TableName string + PrimaryKey sqlschema.PrimaryKey +} + +var _ Operation = (*DropPrimaryKeyOp)(nil) + +func (op *DropPrimaryKeyOp) GetReverse() Operation { + return &AddPrimaryKeyOp{ + TableName: op.TableName, + PrimaryKey: op.PrimaryKey, + } +} + +// AddPrimaryKeyOp adds a new PRIMARY KEY to the table. +type AddPrimaryKeyOp struct { + TableName string + PrimaryKey sqlschema.PrimaryKey +} + +var _ Operation = (*AddPrimaryKeyOp)(nil) + +func (op *AddPrimaryKeyOp) GetReverse() Operation { + return &DropPrimaryKeyOp{ + TableName: op.TableName, + PrimaryKey: op.PrimaryKey, + } +} + +func (op *AddPrimaryKeyOp) DependsOn(another Operation) bool { + switch another := another.(type) { + case *AddColumnOp: + return op.TableName == another.TableName && op.PrimaryKey.Columns.Contains(another.ColumnName) + } + return false +} + +// ChangePrimaryKeyOp changes the PRIMARY KEY of the table. +type ChangePrimaryKeyOp struct { + TableName string + Old sqlschema.PrimaryKey + New sqlschema.PrimaryKey +} + +var _ Operation = (*AddPrimaryKeyOp)(nil) + +func (op *ChangePrimaryKeyOp) GetReverse() Operation { + return &ChangePrimaryKeyOp{ + TableName: op.TableName, + Old: op.New, + New: op.Old, + } +} + +// comment denotes an Operation that cannot be executed. +// +// Operations, which cannot be reversed due to current technical limitations, +// may return &comment with a helpful message from their GetReverse() method. +// +// Chnagelog should skip it when applying operations or output as log message, +// and write it as an SQL comment when creating migration files. +type comment string + +var _ Operation = (*comment)(nil) + +func (c *comment) GetReverse() Operation { return c } diff --git a/vendor/github.com/uptrace/bun/migrate/sqlschema/column.go b/vendor/github.com/uptrace/bun/migrate/sqlschema/column.go new file mode 100644 index 000000000..60f7ea8a6 --- /dev/null +++ b/vendor/github.com/uptrace/bun/migrate/sqlschema/column.go @@ -0,0 +1,75 @@ +package sqlschema + +import ( + "fmt" + + "github.com/uptrace/bun/schema" +) + +type Column interface { + GetName() string + GetSQLType() string + GetVarcharLen() int + GetDefaultValue() string + GetIsNullable() bool + GetIsAutoIncrement() bool + GetIsIdentity() bool + AppendQuery(schema.Formatter, []byte) ([]byte, error) +} + +var _ Column = (*BaseColumn)(nil) + +// BaseColumn is a base column definition that stores various attributes of a column. +// +// Dialects and only dialects can use it to implement the Column interface. +// Other packages must use the Column interface. +type BaseColumn struct { + Name string + SQLType string + VarcharLen int + DefaultValue string + IsNullable bool + IsAutoIncrement bool + IsIdentity bool + // TODO: add Precision and Cardinality for timestamps/bit-strings/floats and arrays respectively. +} + +func (cd BaseColumn) GetName() string { + return cd.Name +} + +func (cd BaseColumn) GetSQLType() string { + return cd.SQLType +} + +func (cd BaseColumn) GetVarcharLen() int { + return cd.VarcharLen +} + +func (cd BaseColumn) GetDefaultValue() string { + return cd.DefaultValue +} + +func (cd BaseColumn) GetIsNullable() bool { + return cd.IsNullable +} + +func (cd BaseColumn) GetIsAutoIncrement() bool { + return cd.IsAutoIncrement +} + +func (cd BaseColumn) GetIsIdentity() bool { + return cd.IsIdentity +} + +// AppendQuery appends full SQL data type. +func (c *BaseColumn) AppendQuery(fmter schema.Formatter, b []byte) (_ []byte, err error) { + b = append(b, c.SQLType...) + if c.VarcharLen == 0 { + return b, nil + } + b = append(b, "("...) + b = append(b, fmt.Sprint(c.VarcharLen)...) + b = append(b, ")"...) + return b, nil +} diff --git a/vendor/github.com/uptrace/bun/migrate/sqlschema/database.go b/vendor/github.com/uptrace/bun/migrate/sqlschema/database.go new file mode 100644 index 000000000..cdc5b2d50 --- /dev/null +++ b/vendor/github.com/uptrace/bun/migrate/sqlschema/database.go @@ -0,0 +1,127 @@ +package sqlschema + +import ( + "slices" + "strings" + + "github.com/uptrace/bun/schema" + orderedmap "github.com/wk8/go-ordered-map/v2" +) + +type Database interface { + GetTables() *orderedmap.OrderedMap[string, Table] + GetForeignKeys() map[ForeignKey]string +} + +var _ Database = (*BaseDatabase)(nil) + +// BaseDatabase is a base database definition. +// +// Dialects and only dialects can use it to implement the Database interface. +// Other packages must use the Database interface. +type BaseDatabase struct { + Tables *orderedmap.OrderedMap[string, Table] + ForeignKeys map[ForeignKey]string +} + +func (ds BaseDatabase) GetTables() *orderedmap.OrderedMap[string, Table] { + return ds.Tables +} + +func (ds BaseDatabase) GetForeignKeys() map[ForeignKey]string { + return ds.ForeignKeys +} + +type ForeignKey struct { + From ColumnReference + To ColumnReference +} + +func NewColumnReference(tableName string, columns ...string) ColumnReference { + return ColumnReference{ + TableName: tableName, + Column: NewColumns(columns...), + } +} + +func (fk ForeignKey) DependsOnTable(tableName string) bool { + return fk.From.TableName == tableName || fk.To.TableName == tableName +} + +func (fk ForeignKey) DependsOnColumn(tableName string, column string) bool { + return fk.DependsOnTable(tableName) && + (fk.From.Column.Contains(column) || fk.To.Column.Contains(column)) +} + +// Columns is a hashable representation of []string used to define schema constraints that depend on multiple columns. +// Although having duplicated column references in these constraints is illegal, Columns neither validates nor enforces this constraint on the caller. +type Columns string + +// NewColumns creates a composite column from a slice of column names. +func NewColumns(columns ...string) Columns { + slices.Sort(columns) + return Columns(strings.Join(columns, ",")) +} + +func (c *Columns) String() string { + return string(*c) +} + +func (c *Columns) AppendQuery(fmter schema.Formatter, b []byte) ([]byte, error) { + return schema.Safe(*c).AppendQuery(fmter, b) +} + +// Split returns a slice of column names that make up the composite. +func (c *Columns) Split() []string { + return strings.Split(c.String(), ",") +} + +// ContainsColumns checks that columns in "other" are a subset of current colums. +func (c *Columns) ContainsColumns(other Columns) bool { + columns := c.Split() +Outer: + for _, check := range other.Split() { + for _, column := range columns { + if check == column { + continue Outer + } + } + return false + } + return true +} + +// Contains checks that a composite column contains the current column. +func (c *Columns) Contains(other string) bool { + return c.ContainsColumns(Columns(other)) +} + +// Replace renames a column if it is part of the composite. +// If a composite consists of multiple columns, only one column will be renamed. +func (c *Columns) Replace(oldColumn, newColumn string) bool { + columns := c.Split() + for i, column := range columns { + if column == oldColumn { + columns[i] = newColumn + *c = NewColumns(columns...) + return true + } + } + return false +} + +// Unique represents a unique constraint defined on 1 or more columns. +type Unique struct { + Name string + Columns Columns +} + +// Equals checks that two unique constraint are the same, assuming both are defined for the same table. +func (u Unique) Equals(other Unique) bool { + return u.Columns == other.Columns +} + +type ColumnReference struct { + TableName string + Column Columns +} diff --git a/vendor/github.com/uptrace/bun/migrate/sqlschema/inspector.go b/vendor/github.com/uptrace/bun/migrate/sqlschema/inspector.go new file mode 100644 index 000000000..fc9af06fc --- /dev/null +++ b/vendor/github.com/uptrace/bun/migrate/sqlschema/inspector.go @@ -0,0 +1,241 @@ +package sqlschema + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/uptrace/bun" + "github.com/uptrace/bun/schema" + orderedmap "github.com/wk8/go-ordered-map/v2" +) + +type InspectorDialect interface { + schema.Dialect + + // Inspector returns a new instance of Inspector for the dialect. + // Dialects MAY set their default InspectorConfig values in constructor + // but MUST apply InspectorOptions to ensure they can be overriden. + // + // Use ApplyInspectorOptions to reduce boilerplate. + NewInspector(db *bun.DB, options ...InspectorOption) Inspector + + // CompareType returns true if col1 and co2 SQL types are equivalent, + // i.e. they might use dialect-specifc type aliases (SERIAL ~ SMALLINT) + // or specify the same VARCHAR length differently (VARCHAR(255) ~ VARCHAR). + CompareType(Column, Column) bool +} + +// InspectorConfig controls the scope of migration by limiting the objects Inspector should return. +// Inspectors SHOULD use the configuration directly instead of copying it, or MAY choose to embed it, +// to make sure options are always applied correctly. +type InspectorConfig struct { + // SchemaName limits inspection to tables in a particular schema. + SchemaName string + + // ExcludeTables from inspection. + ExcludeTables []string +} + +// Inspector reads schema state. +type Inspector interface { + Inspect(ctx context.Context) (Database, error) +} + +func WithSchemaName(schemaName string) InspectorOption { + return func(cfg *InspectorConfig) { + cfg.SchemaName = schemaName + } +} + +// WithExcludeTables works in append-only mode, i.e. tables cannot be re-included. +func WithExcludeTables(tables ...string) InspectorOption { + return func(cfg *InspectorConfig) { + cfg.ExcludeTables = append(cfg.ExcludeTables, tables...) + } +} + +// NewInspector creates a new database inspector, if the dialect supports it. +func NewInspector(db *bun.DB, options ...InspectorOption) (Inspector, error) { + dialect, ok := (db.Dialect()).(InspectorDialect) + if !ok { + return nil, fmt.Errorf("%s does not implement sqlschema.Inspector", db.Dialect().Name()) + } + return &inspector{ + Inspector: dialect.NewInspector(db, options...), + }, nil +} + +func NewBunModelInspector(tables *schema.Tables, options ...InspectorOption) *BunModelInspector { + bmi := &BunModelInspector{ + tables: tables, + } + ApplyInspectorOptions(&bmi.InspectorConfig, options...) + return bmi +} + +type InspectorOption func(*InspectorConfig) + +func ApplyInspectorOptions(cfg *InspectorConfig, options ...InspectorOption) { + for _, opt := range options { + opt(cfg) + } +} + +// inspector is opaque pointer to a database inspector. +type inspector struct { + Inspector +} + +// BunModelInspector creates the current project state from the passed bun.Models. +// Do not recycle BunModelInspector for different sets of models, as older models will not be de-registerred before the next run. +type BunModelInspector struct { + InspectorConfig + tables *schema.Tables +} + +var _ Inspector = (*BunModelInspector)(nil) + +func (bmi *BunModelInspector) Inspect(ctx context.Context) (Database, error) { + state := BunModelSchema{ + BaseDatabase: BaseDatabase{ + ForeignKeys: make(map[ForeignKey]string), + }, + Tables: orderedmap.New[string, Table](), + } + for _, t := range bmi.tables.All() { + if t.Schema != bmi.SchemaName { + continue + } + + columns := orderedmap.New[string, Column]() + for _, f := range t.Fields { + + sqlType, length, err := parseLen(f.CreateTableSQLType) + if err != nil { + return nil, fmt.Errorf("parse length in %q: %w", f.CreateTableSQLType, err) + } + columns.Set(f.Name, &BaseColumn{ + Name: f.Name, + SQLType: strings.ToLower(sqlType), // TODO(dyma): maybe this is not necessary after Column.Eq() + VarcharLen: length, + DefaultValue: exprToLower(f.SQLDefault), + IsNullable: !f.NotNull, + IsAutoIncrement: f.AutoIncrement, + IsIdentity: f.Identity, + }) + } + + var unique []Unique + for name, group := range t.Unique { + // Create a separate unique index for single-column unique constraints + // let each dialect apply the default naming convention. + if name == "" { + for _, f := range group { + unique = append(unique, Unique{Columns: NewColumns(f.Name)}) + } + continue + } + + // Set the name if it is a "unique group", in which case the user has provided the name. + var columns []string + for _, f := range group { + columns = append(columns, f.Name) + } + unique = append(unique, Unique{Name: name, Columns: NewColumns(columns...)}) + } + + var pk *PrimaryKey + if len(t.PKs) > 0 { + var columns []string + for _, f := range t.PKs { + columns = append(columns, f.Name) + } + pk = &PrimaryKey{Columns: NewColumns(columns...)} + } + + // In cases where a table is defined in a non-default schema in the `bun:table` tag, + // schema.Table only extracts the name of the schema, but passes the entire tag value to t.Name + // for backwads-compatibility. For example, a bun model like this: + // type Model struct { bun.BaseModel `bun:"table:favourite.books` } + // produces + // schema.Table{ Schema: "favourite", Name: "favourite.books" } + tableName := strings.TrimPrefix(t.Name, t.Schema+".") + state.Tables.Set(tableName, &BunTable{ + BaseTable: BaseTable{ + Schema: t.Schema, + Name: tableName, + Columns: columns, + UniqueConstraints: unique, + PrimaryKey: pk, + }, + Model: t.ZeroIface, + }) + + for _, rel := range t.Relations { + // These relations are nominal and do not need a foreign key to be declared in the current table. + // They will be either expressed as N:1 relations in an m2m mapping table, or will be referenced by the other table if it's a 1:N. + if rel.Type == schema.ManyToManyRelation || + rel.Type == schema.HasManyRelation { + continue + } + + var fromCols, toCols []string + for _, f := range rel.BasePKs { + fromCols = append(fromCols, f.Name) + } + for _, f := range rel.JoinPKs { + toCols = append(toCols, f.Name) + } + + target := rel.JoinTable + state.ForeignKeys[ForeignKey{ + From: NewColumnReference(t.Name, fromCols...), + To: NewColumnReference(target.Name, toCols...), + }] = "" + } + } + return state, nil +} + +func parseLen(typ string) (string, int, error) { + paren := strings.Index(typ, "(") + if paren == -1 { + return typ, 0, nil + } + length, err := strconv.Atoi(typ[paren+1 : len(typ)-1]) + if err != nil { + return typ, 0, err + } + return typ[:paren], length, nil +} + +// exprToLower converts string to lowercase, if it does not contain a string literal 'lit'. +// Use it to ensure that user-defined default values in the models are always comparable +// to those returned by the database inspector, regardless of the case convention in individual drivers. +func exprToLower(s string) string { + if strings.HasPrefix(s, "'") && strings.HasSuffix(s, "'") { + return s + } + return strings.ToLower(s) +} + +// BunModelSchema is the schema state derived from bun table models. +type BunModelSchema struct { + BaseDatabase + + Tables *orderedmap.OrderedMap[string, Table] +} + +func (ms BunModelSchema) GetTables() *orderedmap.OrderedMap[string, Table] { + return ms.Tables +} + +// BunTable provides additional table metadata that is only accessible from scanning bun models. +type BunTable struct { + BaseTable + + // Model stores the zero interface to the underlying Go struct. + Model interface{} +} diff --git a/vendor/github.com/uptrace/bun/migrate/sqlschema/migrator.go b/vendor/github.com/uptrace/bun/migrate/sqlschema/migrator.go new file mode 100644 index 000000000..00500061b --- /dev/null +++ b/vendor/github.com/uptrace/bun/migrate/sqlschema/migrator.go @@ -0,0 +1,49 @@ +package sqlschema + +import ( + "fmt" + + "github.com/uptrace/bun" + "github.com/uptrace/bun/schema" +) + +type MigratorDialect interface { + schema.Dialect + NewMigrator(db *bun.DB, schemaName string) Migrator +} + +type Migrator interface { + AppendSQL(b []byte, operation interface{}) ([]byte, error) +} + +// migrator is a dialect-agnostic wrapper for sqlschema.MigratorDialect. +type migrator struct { + Migrator +} + +func NewMigrator(db *bun.DB, schemaName string) (Migrator, error) { + md, ok := db.Dialect().(MigratorDialect) + if !ok { + return nil, fmt.Errorf("%q dialect does not implement sqlschema.Migrator", db.Dialect().Name()) + } + return &migrator{ + Migrator: md.NewMigrator(db, schemaName), + }, nil +} + +// BaseMigrator can be embeded by dialect's Migrator implementations to re-use some of the existing bun queries. +type BaseMigrator struct { + db *bun.DB +} + +func NewBaseMigrator(db *bun.DB) *BaseMigrator { + return &BaseMigrator{db: db} +} + +func (m *BaseMigrator) AppendCreateTable(b []byte, model interface{}) ([]byte, error) { + return m.db.NewCreateTable().Model(model).AppendQuery(m.db.Formatter(), b) +} + +func (m *BaseMigrator) AppendDropTable(b []byte, schemaName, tableName string) ([]byte, error) { + return m.db.NewDropTable().TableExpr("?.?", bun.Ident(schemaName), bun.Ident(tableName)).AppendQuery(m.db.Formatter(), b) +} diff --git a/vendor/github.com/uptrace/bun/migrate/sqlschema/table.go b/vendor/github.com/uptrace/bun/migrate/sqlschema/table.go new file mode 100644 index 000000000..a805ba780 --- /dev/null +++ b/vendor/github.com/uptrace/bun/migrate/sqlschema/table.go @@ -0,0 +1,60 @@ +package sqlschema + +import ( + orderedmap "github.com/wk8/go-ordered-map/v2" +) + +type Table interface { + GetSchema() string + GetName() string + GetColumns() *orderedmap.OrderedMap[string, Column] + GetPrimaryKey() *PrimaryKey + GetUniqueConstraints() []Unique +} + +var _ Table = (*BaseTable)(nil) + +// BaseTable is a base table definition. +// +// Dialects and only dialects can use it to implement the Table interface. +// Other packages must use the Table interface. +type BaseTable struct { + Schema string + Name string + + // ColumnDefinitions map each column name to the column definition. + Columns *orderedmap.OrderedMap[string, Column] + + // PrimaryKey holds the primary key definition. + // A nil value means that no primary key is defined for the table. + PrimaryKey *PrimaryKey + + // UniqueConstraints defined on the table. + UniqueConstraints []Unique +} + +// PrimaryKey represents a primary key constraint defined on 1 or more columns. +type PrimaryKey struct { + Name string + Columns Columns +} + +func (td *BaseTable) GetSchema() string { + return td.Schema +} + +func (td *BaseTable) GetName() string { + return td.Name +} + +func (td *BaseTable) GetColumns() *orderedmap.OrderedMap[string, Column] { + return td.Columns +} + +func (td *BaseTable) GetPrimaryKey() *PrimaryKey { + return td.PrimaryKey +} + +func (td *BaseTable) GetUniqueConstraints() []Unique { + return td.UniqueConstraints +} diff --git a/vendor/github.com/uptrace/bun/model_table_has_many.go b/vendor/github.com/uptrace/bun/model_table_has_many.go index 544cdf5d6..cd721a1b2 100644 --- a/vendor/github.com/uptrace/bun/model_table_has_many.go +++ b/vendor/github.com/uptrace/bun/model_table_has_many.go @@ -51,7 +51,7 @@ func (m *hasManyModel) ScanRows(ctx context.Context, rows *sql.Rows) (int, error dest := makeDest(m, len(columns)) var n int - + m.structKey = make([]interface{}, len(m.rel.JoinPKs)) for rows.Next() { if m.sliceOfPtr { m.strct = reflect.New(m.table.Type).Elem() @@ -59,9 +59,8 @@ func (m *hasManyModel) ScanRows(ctx context.Context, rows *sql.Rows) (int, error m.strct.Set(m.table.ZeroValue) } m.structInited = false - m.scanIndex = 0 - m.structKey = m.structKey[:0] + if err := rows.Scan(dest...); err != nil { return 0, err } @@ -92,9 +91,9 @@ func (m *hasManyModel) Scan(src interface{}) error { return err } - for _, f := range m.rel.JoinPKs { - if f.Name == field.Name { - m.structKey = append(m.structKey, indirectFieldValue(field.Value(m.strct))) + for i, f := range m.rel.JoinPKs { + if f.Name == column { + m.structKey[i] = indirectFieldValue(field.Value(m.strct)) break } } diff --git a/vendor/github.com/uptrace/bun/package.json b/vendor/github.com/uptrace/bun/package.json index 6a8d7082e..15c6fd47b 100644 --- a/vendor/github.com/uptrace/bun/package.json +++ b/vendor/github.com/uptrace/bun/package.json @@ -1,6 +1,6 @@ { "name": "gobun", - "version": "1.2.5", + "version": "1.2.6", "main": "index.js", "repository": "git@github.com:uptrace/bun.git", "author": "Vladimir Mihailenco ", diff --git a/vendor/github.com/uptrace/bun/query_base.go b/vendor/github.com/uptrace/bun/query_base.go index 8a26a4c8a..52b0c1e22 100644 --- a/vendor/github.com/uptrace/bun/query_base.go +++ b/vendor/github.com/uptrace/bun/query_base.go @@ -6,6 +6,8 @@ import ( "database/sql/driver" "errors" "fmt" + "strconv" + "strings" "time" "github.com/uptrace/bun/dialect" @@ -1352,3 +1354,113 @@ func (ih *idxHintsQuery) bufIndexHint( b = append(b, ")"...) return b, nil } + +//------------------------------------------------------------------------------ + +type orderLimitOffsetQuery struct { + order []schema.QueryWithArgs + + limit int32 + offset int32 +} + +func (q *orderLimitOffsetQuery) addOrder(orders ...string) { + for _, order := range orders { + if order == "" { + continue + } + + index := strings.IndexByte(order, ' ') + if index == -1 { + q.order = append(q.order, schema.UnsafeIdent(order)) + continue + } + + field := order[:index] + sort := order[index+1:] + + switch strings.ToUpper(sort) { + case "ASC", "DESC", "ASC NULLS FIRST", "DESC NULLS FIRST", + "ASC NULLS LAST", "DESC NULLS LAST": + q.order = append(q.order, schema.SafeQuery("? ?", []interface{}{ + Ident(field), + Safe(sort), + })) + default: + q.order = append(q.order, schema.UnsafeIdent(order)) + } + } + +} + +func (q *orderLimitOffsetQuery) addOrderExpr(query string, args ...interface{}) { + q.order = append(q.order, schema.SafeQuery(query, args)) +} + +func (q *orderLimitOffsetQuery) appendOrder(fmter schema.Formatter, b []byte) (_ []byte, err error) { + if len(q.order) > 0 { + b = append(b, " ORDER BY "...) + + for i, f := range q.order { + if i > 0 { + b = append(b, ", "...) + } + b, err = f.AppendQuery(fmter, b) + if err != nil { + return nil, err + } + } + + return b, nil + } + + // MSSQL: allows Limit() without Order() as per https://stackoverflow.com/a/36156953 + if q.limit > 0 && fmter.Dialect().Name() == dialect.MSSQL { + return append(b, " ORDER BY _temp_sort"...), nil + } + + return b, nil +} + +func (q *orderLimitOffsetQuery) setLimit(n int) { + q.limit = int32(n) +} + +func (q *orderLimitOffsetQuery) setOffset(n int) { + q.offset = int32(n) +} + +func (q *orderLimitOffsetQuery) appendLimitOffset(fmter schema.Formatter, b []byte) (_ []byte, err error) { + if fmter.Dialect().Features().Has(feature.OffsetFetch) { + if q.limit > 0 && q.offset > 0 { + b = append(b, " OFFSET "...) + b = strconv.AppendInt(b, int64(q.offset), 10) + b = append(b, " ROWS"...) + + b = append(b, " FETCH NEXT "...) + b = strconv.AppendInt(b, int64(q.limit), 10) + b = append(b, " ROWS ONLY"...) + } else if q.limit > 0 { + b = append(b, " OFFSET 0 ROWS"...) + + b = append(b, " FETCH NEXT "...) + b = strconv.AppendInt(b, int64(q.limit), 10) + b = append(b, " ROWS ONLY"...) + } else if q.offset > 0 { + b = append(b, " OFFSET "...) + b = strconv.AppendInt(b, int64(q.offset), 10) + b = append(b, " ROWS"...) + } + } else { + if q.limit > 0 { + b = append(b, " LIMIT "...) + b = strconv.AppendInt(b, int64(q.limit), 10) + } + if q.offset > 0 { + b = append(b, " OFFSET "...) + b = strconv.AppendInt(b, int64(q.offset), 10) + } + } + + return b, nil +} diff --git a/vendor/github.com/uptrace/bun/query_column_add.go b/vendor/github.com/uptrace/bun/query_column_add.go index 32a21338e..50576873c 100644 --- a/vendor/github.com/uptrace/bun/query_column_add.go +++ b/vendor/github.com/uptrace/bun/query_column_add.go @@ -42,9 +42,12 @@ func (q *AddColumnQuery) Err(err error) *AddColumnQuery { return q } -func (q *AddColumnQuery) Apply(fn func(*AddColumnQuery) *AddColumnQuery) *AddColumnQuery { - if fn != nil { - return fn(q) +// Apply calls each function in fns, passing the AddColumnQuery as an argument. +func (q *AddColumnQuery) Apply(fns ...func(*AddColumnQuery) *AddColumnQuery) *AddColumnQuery { + for _, fn := range fns { + if fn != nil { + q = fn(q) + } } return q } diff --git a/vendor/github.com/uptrace/bun/query_column_drop.go b/vendor/github.com/uptrace/bun/query_column_drop.go index 1439ed9b9..24fc93cfd 100644 --- a/vendor/github.com/uptrace/bun/query_column_drop.go +++ b/vendor/github.com/uptrace/bun/query_column_drop.go @@ -40,9 +40,12 @@ func (q *DropColumnQuery) Err(err error) *DropColumnQuery { return q } -func (q *DropColumnQuery) Apply(fn func(*DropColumnQuery) *DropColumnQuery) *DropColumnQuery { - if fn != nil { - return fn(q) +// Apply calls each function in fns, passing the DropColumnQuery as an argument. +func (q *DropColumnQuery) Apply(fns ...func(*DropColumnQuery) *DropColumnQuery) *DropColumnQuery { + for _, fn := range fns { + if fn != nil { + q = fn(q) + } } return q } diff --git a/vendor/github.com/uptrace/bun/query_delete.go b/vendor/github.com/uptrace/bun/query_delete.go index 49a750cc8..ccfeb1997 100644 --- a/vendor/github.com/uptrace/bun/query_delete.go +++ b/vendor/github.com/uptrace/bun/query_delete.go @@ -3,6 +3,7 @@ package bun import ( "context" "database/sql" + "errors" "time" "github.com/uptrace/bun/dialect/feature" @@ -12,6 +13,7 @@ import ( type DeleteQuery struct { whereBaseQuery + orderLimitOffsetQuery returningQuery } @@ -44,10 +46,12 @@ func (q *DeleteQuery) Err(err error) *DeleteQuery { return q } -// Apply calls the fn passing the DeleteQuery as an argument. -func (q *DeleteQuery) Apply(fn func(*DeleteQuery) *DeleteQuery) *DeleteQuery { - if fn != nil { - return fn(q) +// Apply calls each function in fns, passing the DeleteQuery as an argument. +func (q *DeleteQuery) Apply(fns ...func(*DeleteQuery) *DeleteQuery) *DeleteQuery { + for _, fn := range fns { + if fn != nil { + q = fn(q) + } } return q } @@ -120,17 +124,50 @@ func (q *DeleteQuery) WhereAllWithDeleted() *DeleteQuery { return q } +func (q *DeleteQuery) Order(orders ...string) *DeleteQuery { + if !q.hasFeature(feature.DeleteOrderLimit) { + q.err = errors.New("bun: order is not supported for current dialect") + return q + } + q.addOrder(orders...) + return q +} + +func (q *DeleteQuery) OrderExpr(query string, args ...interface{}) *DeleteQuery { + if !q.hasFeature(feature.DeleteOrderLimit) { + q.err = errors.New("bun: order is not supported for current dialect") + return q + } + q.addOrderExpr(query, args...) + return q +} + func (q *DeleteQuery) ForceDelete() *DeleteQuery { q.flags = q.flags.Set(forceDeleteFlag) return q } +// ------------------------------------------------------------------------------ +func (q *DeleteQuery) Limit(n int) *DeleteQuery { + if !q.hasFeature(feature.DeleteOrderLimit) { + q.err = errors.New("bun: limit is not supported for current dialect") + return q + } + q.setLimit(n) + return q +} + //------------------------------------------------------------------------------ // Returning adds a RETURNING clause to the query. // // To suppress the auto-generated RETURNING clause, use `Returning("NULL")`. func (q *DeleteQuery) Returning(query string, args ...interface{}) *DeleteQuery { + if !q.hasFeature(feature.DeleteReturning) { + q.err = errors.New("bun: returning is not supported for current dialect") + return q + } + q.addReturning(schema.SafeQuery(query, args)) return q } @@ -203,7 +240,21 @@ func (q *DeleteQuery) AppendQuery(fmter schema.Formatter, b []byte) (_ []byte, e return nil, err } - if q.hasFeature(feature.Returning) && q.hasReturning() { + if q.hasMultiTables() && (len(q.order) > 0 || q.limit > 0) { + return nil, errors.New("bun: can't use ORDER or LIMIT with multiple tables") + } + + b, err = q.appendOrder(fmter, b) + if err != nil { + return nil, err + } + + b, err = q.appendLimitOffset(fmter, b) + if err != nil { + return nil, err + } + + if q.hasFeature(feature.DeleteReturning) && q.hasReturning() { b = append(b, " RETURNING "...) b, err = q.appendReturning(fmter, b) if err != nil { @@ -265,7 +316,7 @@ func (q *DeleteQuery) scanOrExec( return nil, err } - useScan := hasDest || (q.hasReturning() && q.hasFeature(feature.Returning|feature.Output)) + useScan := hasDest || (q.hasReturning() && q.hasFeature(feature.DeleteReturning|feature.Output)) var model Model if useScan { diff --git a/vendor/github.com/uptrace/bun/query_insert.go b/vendor/github.com/uptrace/bun/query_insert.go index 6d38a4efe..b6747cd65 100644 --- a/vendor/github.com/uptrace/bun/query_insert.go +++ b/vendor/github.com/uptrace/bun/query_insert.go @@ -53,10 +53,12 @@ func (q *InsertQuery) Err(err error) *InsertQuery { return q } -// Apply calls the fn passing the SelectQuery as an argument. -func (q *InsertQuery) Apply(fn func(*InsertQuery) *InsertQuery) *InsertQuery { - if fn != nil { - return fn(q) +// Apply calls each function in fns, passing the InsertQuery as an argument. +func (q *InsertQuery) Apply(fns ...func(*InsertQuery) *InsertQuery) *InsertQuery { + for _, fn := range fns { + if fn != nil { + q = fn(q) + } } return q } diff --git a/vendor/github.com/uptrace/bun/query_merge.go b/vendor/github.com/uptrace/bun/query_merge.go index 626752b8a..3c3f4f7f8 100644 --- a/vendor/github.com/uptrace/bun/query_merge.go +++ b/vendor/github.com/uptrace/bun/query_merge.go @@ -50,10 +50,12 @@ func (q *MergeQuery) Err(err error) *MergeQuery { return q } -// Apply calls the fn passing the MergeQuery as an argument. -func (q *MergeQuery) Apply(fn func(*MergeQuery) *MergeQuery) *MergeQuery { - if fn != nil { - return fn(q) +// Apply calls each function in fns, passing the MergeQuery as an argument. +func (q *MergeQuery) Apply(fns ...func(*MergeQuery) *MergeQuery) *MergeQuery { + for _, fn := range fns { + if fn != nil { + q = fn(q) + } } return q } diff --git a/vendor/github.com/uptrace/bun/query_raw.go b/vendor/github.com/uptrace/bun/query_raw.go index fda088a7c..1634d0e5b 100644 --- a/vendor/github.com/uptrace/bun/query_raw.go +++ b/vendor/github.com/uptrace/bun/query_raw.go @@ -96,3 +96,12 @@ func (q *RawQuery) AppendQuery(fmter schema.Formatter, b []byte) ([]byte, error) func (q *RawQuery) Operation() string { return "SELECT" } + +func (q *RawQuery) String() string { + buf, err := q.AppendQuery(q.db.Formatter(), nil) + if err != nil { + panic(err) + } + + return string(buf) +} diff --git a/vendor/github.com/uptrace/bun/query_select.go b/vendor/github.com/uptrace/bun/query_select.go index 5bb329143..2b0872ae0 100644 --- a/vendor/github.com/uptrace/bun/query_select.go +++ b/vendor/github.com/uptrace/bun/query_select.go @@ -6,8 +6,6 @@ import ( "database/sql" "errors" "fmt" - "strconv" - "strings" "sync" "github.com/uptrace/bun/dialect" @@ -25,14 +23,12 @@ type union struct { type SelectQuery struct { whereBaseQuery idxHintsQuery + orderLimitOffsetQuery distinctOn []schema.QueryWithArgs joins []joinQuery group []schema.QueryWithArgs having []schema.QueryWithArgs - order []schema.QueryWithArgs - limit int32 - offset int32 selFor schema.QueryWithArgs union []union @@ -66,10 +62,12 @@ func (q *SelectQuery) Err(err error) *SelectQuery { return q } -// Apply calls the fn passing the SelectQuery as an argument. -func (q *SelectQuery) Apply(fn func(*SelectQuery) *SelectQuery) *SelectQuery { - if fn != nil { - return fn(q) +// Apply calls each function in fns, passing the SelectQuery as an argument. +func (q *SelectQuery) Apply(fns ...func(*SelectQuery) *SelectQuery) *SelectQuery { + for _, fn := range fns { + if fn != nil { + q = fn(q) + } } return q } @@ -279,46 +277,22 @@ func (q *SelectQuery) Having(having string, args ...interface{}) *SelectQuery { } func (q *SelectQuery) Order(orders ...string) *SelectQuery { - for _, order := range orders { - if order == "" { - continue - } - - index := strings.IndexByte(order, ' ') - if index == -1 { - q.order = append(q.order, schema.UnsafeIdent(order)) - continue - } - - field := order[:index] - sort := order[index+1:] - - switch strings.ToUpper(sort) { - case "ASC", "DESC", "ASC NULLS FIRST", "DESC NULLS FIRST", - "ASC NULLS LAST", "DESC NULLS LAST": - q.order = append(q.order, schema.SafeQuery("? ?", []interface{}{ - Ident(field), - Safe(sort), - })) - default: - q.order = append(q.order, schema.UnsafeIdent(order)) - } - } + q.addOrder(orders...) return q } func (q *SelectQuery) OrderExpr(query string, args ...interface{}) *SelectQuery { - q.order = append(q.order, schema.SafeQuery(query, args)) + q.addOrderExpr(query, args...) return q } func (q *SelectQuery) Limit(n int) *SelectQuery { - q.limit = int32(n) + q.setLimit(n) return q } func (q *SelectQuery) Offset(n int) *SelectQuery { - q.offset = int32(n) + q.setOffset(n) return q } @@ -615,35 +589,9 @@ func (q *SelectQuery) appendQuery( return nil, err } - if fmter.Dialect().Features().Has(feature.OffsetFetch) { - if q.limit > 0 && q.offset > 0 { - b = append(b, " OFFSET "...) - b = strconv.AppendInt(b, int64(q.offset), 10) - b = append(b, " ROWS"...) - - b = append(b, " FETCH NEXT "...) - b = strconv.AppendInt(b, int64(q.limit), 10) - b = append(b, " ROWS ONLY"...) - } else if q.limit > 0 { - b = append(b, " OFFSET 0 ROWS"...) - - b = append(b, " FETCH NEXT "...) - b = strconv.AppendInt(b, int64(q.limit), 10) - b = append(b, " ROWS ONLY"...) - } else if q.offset > 0 { - b = append(b, " OFFSET "...) - b = strconv.AppendInt(b, int64(q.offset), 10) - b = append(b, " ROWS"...) - } - } else { - if q.limit > 0 { - b = append(b, " LIMIT "...) - b = strconv.AppendInt(b, int64(q.limit), 10) - } - if q.offset > 0 { - b = append(b, " OFFSET "...) - b = strconv.AppendInt(b, int64(q.offset), 10) - } + b, err = q.appendLimitOffset(fmter, b) + if err != nil { + return nil, err } if !q.selFor.IsZero() { @@ -782,31 +730,6 @@ func (q *SelectQuery) appendTables(fmter schema.Formatter, b []byte) (_ []byte, return q.appendTablesWithAlias(fmter, b) } -func (q *SelectQuery) appendOrder(fmter schema.Formatter, b []byte) (_ []byte, err error) { - if len(q.order) > 0 { - b = append(b, " ORDER BY "...) - - for i, f := range q.order { - if i > 0 { - b = append(b, ", "...) - } - b, err = f.AppendQuery(fmter, b) - if err != nil { - return nil, err - } - } - - return b, nil - } - - // MSSQL: allows Limit() without Order() as per https://stackoverflow.com/a/36156953 - if q.limit > 0 && fmter.Dialect().Name() == dialect.MSSQL { - return append(b, " ORDER BY _temp_sort"...), nil - } - - return b, nil -} - //------------------------------------------------------------------------------ func (q *SelectQuery) Rows(ctx context.Context) (*sql.Rows, error) { diff --git a/vendor/github.com/uptrace/bun/query_table_drop.go b/vendor/github.com/uptrace/bun/query_table_drop.go index e4447a8d2..a92014515 100644 --- a/vendor/github.com/uptrace/bun/query_table_drop.go +++ b/vendor/github.com/uptrace/bun/query_table_drop.go @@ -151,3 +151,12 @@ func (q *DropTableQuery) afterDropTableHook(ctx context.Context) error { } return nil } + +func (q *DropTableQuery) String() string { + buf, err := q.AppendQuery(q.db.Formatter(), nil) + if err != nil { + panic(err) + } + + return string(buf) +} diff --git a/vendor/github.com/uptrace/bun/query_update.go b/vendor/github.com/uptrace/bun/query_update.go index e56ba20d1..bb9264084 100644 --- a/vendor/github.com/uptrace/bun/query_update.go +++ b/vendor/github.com/uptrace/bun/query_update.go @@ -15,6 +15,7 @@ import ( type UpdateQuery struct { whereBaseQuery + orderLimitOffsetQuery returningQuery customValueQuery setQuery @@ -53,10 +54,12 @@ func (q *UpdateQuery) Err(err error) *UpdateQuery { return q } -// Apply calls the fn passing the SelectQuery as an argument. -func (q *UpdateQuery) Apply(fn func(*UpdateQuery) *UpdateQuery) *UpdateQuery { - if fn != nil { - return fn(q) +// Apply calls each function in fns, passing the UpdateQuery as an argument. +func (q *UpdateQuery) Apply(fns ...func(*UpdateQuery) *UpdateQuery) *UpdateQuery { + for _, fn := range fns { + if fn != nil { + q = fn(q) + } } return q } @@ -200,6 +203,34 @@ func (q *UpdateQuery) WhereAllWithDeleted() *UpdateQuery { return q } +// ------------------------------------------------------------------------------ +func (q *UpdateQuery) Order(orders ...string) *UpdateQuery { + if !q.hasFeature(feature.UpdateOrderLimit) { + q.err = errors.New("bun: order is not supported for current dialect") + return q + } + q.addOrder(orders...) + return q +} + +func (q *UpdateQuery) OrderExpr(query string, args ...interface{}) *UpdateQuery { + if !q.hasFeature(feature.UpdateOrderLimit) { + q.err = errors.New("bun: order is not supported for current dialect") + return q + } + q.addOrderExpr(query, args...) + return q +} + +func (q *UpdateQuery) Limit(n int) *UpdateQuery { + if !q.hasFeature(feature.UpdateOrderLimit) { + q.err = errors.New("bun: limit is not supported for current dialect") + return q + } + q.setLimit(n) + return q +} + //------------------------------------------------------------------------------ // Returning adds a RETURNING clause to the query. @@ -278,6 +309,16 @@ func (q *UpdateQuery) AppendQuery(fmter schema.Formatter, b []byte) (_ []byte, e return nil, err } + b, err = q.appendOrder(fmter, b) + if err != nil { + return nil, err + } + + b, err = q.appendLimitOffset(fmter, b) + if err != nil { + return nil, err + } + if q.hasFeature(feature.Returning) && q.hasReturning() { b = append(b, " RETURNING "...) b, err = q.appendReturning(fmter, b) diff --git a/vendor/github.com/uptrace/bun/schema/dialect.go b/vendor/github.com/uptrace/bun/schema/dialect.go index 330293444..bb40af62b 100644 --- a/vendor/github.com/uptrace/bun/schema/dialect.go +++ b/vendor/github.com/uptrace/bun/schema/dialect.go @@ -39,6 +39,9 @@ type Dialect interface { // is mandatory in queries that modify the schema (CREATE TABLE / ADD COLUMN, etc). // Dialects that do not have such requirement may return 0, which should be interpreted so by the caller. DefaultVarcharLen() int + + // DefaultSchema should returns the name of the default database schema. + DefaultSchema() string } // ------------------------------------------------------------------------------ @@ -185,3 +188,7 @@ func (d *nopDialect) DefaultVarcharLen() int { func (d *nopDialect) AppendSequence(b []byte, _ *Table, _ *Field) []byte { return b } + +func (d *nopDialect) DefaultSchema() string { + return "nop" +} diff --git a/vendor/github.com/uptrace/bun/schema/table.go b/vendor/github.com/uptrace/bun/schema/table.go index c8e71e38f..82132c4f1 100644 --- a/vendor/github.com/uptrace/bun/schema/table.go +++ b/vendor/github.com/uptrace/bun/schema/table.go @@ -45,6 +45,7 @@ type Table struct { TypeName string ModelName string + Schema string Name string SQLName Safe SQLNameForSelects Safe @@ -85,6 +86,7 @@ func (table *Table) init(dialect Dialect, typ reflect.Type, canAddr bool) { table.setName(tableName) table.Alias = table.ModelName table.SQLAlias = table.quoteIdent(table.ModelName) + table.Schema = dialect.DefaultSchema() table.Fields = make([]*Field, 0, typ.NumField()) table.FieldMap = make(map[string]*Field, typ.NumField()) @@ -244,6 +246,31 @@ func (t *Table) processFields(typ reflect.Type, canAddr bool) { subfield.SQLName = t.quoteIdent(subfield.Name) } t.addField(subfield) + if v, ok := subfield.Tag.Options["unique"]; ok { + t.addUnique(subfield, embfield.prefix, v) + } + } +} + +func (t *Table) addUnique(field *Field, prefix string, tagOptions []string) { + var names []string + if len(tagOptions) == 1 { + // Split the value by comma, this will allow multiple names to be specified. + // We can use this to create multiple named unique constraints where a single column + // might be included in multiple constraints. + names = strings.Split(tagOptions[0], ",") + } else { + names = tagOptions + } + + for _, uname := range names { + if t.Unique == nil { + t.Unique = make(map[string][]*Field) + } + if uname != "" && prefix != "" { + uname = prefix + uname + } + t.Unique[uname] = append(t.Unique[uname], field) } } @@ -371,10 +398,18 @@ func (t *Table) processBaseModelField(f reflect.StructField) { } if tag.Name != "" { + schema, _ := t.schemaFromTagName(tag.Name) + t.Schema = schema + + // Eventually, we should only assign the "table" portion as the table name, + // which will also require a change in how the table name is appended to queries. + // Until that is done, set table name to tag.Name. t.setName(tag.Name) } if s, ok := tag.Option("table"); ok { + schema, _ := t.schemaFromTagName(s) + t.Schema = schema t.setName(s) } @@ -388,6 +423,17 @@ func (t *Table) processBaseModelField(f reflect.StructField) { } } +// schemaFromTagName splits the bun.BaseModel tag name into schema and table name +// in case it is specified in the "schema"."table" format. +// Assume default schema if one isn't explicitly specified. +func (t *Table) schemaFromTagName(name string) (string, string) { + schema, table := t.dialect.DefaultSchema(), name + if schemaTable := strings.Split(name, "."); len(schemaTable) == 2 { + schema, table = schemaTable[0], schemaTable[1] + } + return schema, table +} + // nolint func (t *Table) newField(sf reflect.StructField, tag tagparser.Tag) *Field { sqlName := internal.Underscore(sf.Name) @@ -439,22 +485,7 @@ func (t *Table) newField(sf reflect.StructField, tag tagparser.Tag) *Field { } if v, ok := tag.Options["unique"]; ok { - var names []string - if len(v) == 1 { - // Split the value by comma, this will allow multiple names to be specified. - // We can use this to create multiple named unique constraints where a single column - // might be included in multiple constraints. - names = strings.Split(v[0], ",") - } else { - names = v - } - - for _, uniqueName := range names { - if t.Unique == nil { - t.Unique = make(map[string][]*Field) - } - t.Unique[uniqueName] = append(t.Unique[uniqueName], field) - } + t.addUnique(field, "", v) } if s, ok := tag.Option("default"); ok { field.SQLDefault = s diff --git a/vendor/github.com/uptrace/bun/schema/tables.go b/vendor/github.com/uptrace/bun/schema/tables.go index 985093421..58c45cbee 100644 --- a/vendor/github.com/uptrace/bun/schema/tables.go +++ b/vendor/github.com/uptrace/bun/schema/tables.go @@ -77,6 +77,7 @@ func (t *Tables) InProgress(typ reflect.Type) *Table { return table } +// ByModel gets the table by its Go name. func (t *Tables) ByModel(name string) *Table { var found *Table t.tables.Range(func(typ reflect.Type, table *Table) bool { @@ -89,6 +90,7 @@ func (t *Tables) ByModel(name string) *Table { return found } +// ByName gets the table by its SQL name. func (t *Tables) ByName(name string) *Table { var found *Table t.tables.Range(func(typ reflect.Type, table *Table) bool { @@ -100,3 +102,13 @@ func (t *Tables) ByName(name string) *Table { }) return found } + +// All returns all registered tables. +func (t *Tables) All() []*Table { + var found []*Table + t.tables.Range(func(typ reflect.Type, table *Table) bool { + found = append(found, table) + return true + }) + return found +} diff --git a/vendor/github.com/uptrace/bun/version.go b/vendor/github.com/uptrace/bun/version.go index 7f23c12c3..244d63465 100644 --- a/vendor/github.com/uptrace/bun/version.go +++ b/vendor/github.com/uptrace/bun/version.go @@ -2,5 +2,5 @@ package bun // Version is the current release version. func Version() string { - return "1.2.5" + return "1.2.6" } diff --git a/vendor/github.com/wk8/go-ordered-map/v2/.gitignore b/vendor/github.com/wk8/go-ordered-map/v2/.gitignore new file mode 100644 index 000000000..57872d0f1 --- /dev/null +++ b/vendor/github.com/wk8/go-ordered-map/v2/.gitignore @@ -0,0 +1 @@ +/vendor/ diff --git a/vendor/github.com/wk8/go-ordered-map/v2/.golangci.yml b/vendor/github.com/wk8/go-ordered-map/v2/.golangci.yml new file mode 100644 index 000000000..f305c9db1 --- /dev/null +++ b/vendor/github.com/wk8/go-ordered-map/v2/.golangci.yml @@ -0,0 +1,78 @@ +run: + tests: false + +linters: + disable-all: true + enable: + - asciicheck + - bidichk + - bodyclose + - containedctx + - contextcheck + - decorder + # Disabling depguard as there is no guarded list of imports + # - depguard + - dogsled + - dupl + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - exportloopref + - forbidigo + - funlen + # Don't need gci and goimports + # - gci + - gochecknoglobals + - gochecknoinits + - gocognit + - goconst + - gocritic + - gocyclo + - godox + - gofmt + - gofumpt + - goheader + - goimports + - mnd + - gomoddirectives + - gomodguard + - goprintffuncname + - gosec + - gosimple + - govet + - grouper + - importas + - ineffassign + - lll + - maintidx + - makezero + - misspell + - nakedret + - nilerr + - nilnil + - noctx + - nolintlint + - paralleltest + - prealloc + - predeclared + - promlinter + - revive + - rowserrcheck + - sqlclosecheck + - staticcheck + - stylecheck + - tagliatelle + - tenv + - testpackage + - thelper + - tparallel + # FIXME: doesn't support 1.23 yet + # - typecheck + - unconvert + - unparam + - unused + - varnamelen + - wastedassign + - whitespace diff --git a/vendor/github.com/wk8/go-ordered-map/v2/CHANGELOG.md b/vendor/github.com/wk8/go-ordered-map/v2/CHANGELOG.md new file mode 100644 index 000000000..f27126f84 --- /dev/null +++ b/vendor/github.com/wk8/go-ordered-map/v2/CHANGELOG.md @@ -0,0 +1,38 @@ +# Changelog + +[comment]: # (Changes since last release go here) + +## 2.1.8 - Jun 27th 2023 + +* Added support for YAML serialization/deserialization + +## 2.1.7 - Apr 13th 2023 + +* Renamed test_utils.go to utils_test.go + +## 2.1.6 - Feb 15th 2023 + +* Added `GetAndMoveToBack()` and `GetAndMoveToFront()` methods + +## 2.1.5 - Dec 13th 2022 + +* Added `Value()` method + +## 2.1.4 - Dec 12th 2022 + +* Fixed a bug with UTF-8 special characters in JSON keys + +## 2.1.3 - Dec 11th 2022 + +* Added support for JSON marshalling/unmarshalling of wrapper of primitive types + +## 2.1.2 - Dec 10th 2022 +* Allowing to pass options to `New`, to give a capacity hint, or initial data +* Allowing to deserialize nested ordered maps from JSON without having to explicitly instantiate them +* Added the `AddPairs` method + +## 2.1.1 - Dec 9th 2022 +* Fixing a bug with JSON marshalling + +## 2.1.0 - Dec 7th 2022 +* Added support for JSON serialization/deserialization diff --git a/vendor/github.com/wk8/go-ordered-map/v2/LICENSE b/vendor/github.com/wk8/go-ordered-map/v2/LICENSE new file mode 100644 index 000000000..8dada3eda --- /dev/null +++ b/vendor/github.com/wk8/go-ordered-map/v2/LICENSE @@ -0,0 +1,201 @@ + 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/wk8/go-ordered-map/v2/Makefile b/vendor/github.com/wk8/go-ordered-map/v2/Makefile new file mode 100644 index 000000000..6e0e18a1b --- /dev/null +++ b/vendor/github.com/wk8/go-ordered-map/v2/Makefile @@ -0,0 +1,32 @@ +.DEFAULT_GOAL := all + +.PHONY: all +all: test_with_fuzz lint + +# the TEST_FLAGS env var can be set to eg run only specific tests +TEST_COMMAND = go test -v -count=1 -race -cover $(TEST_FLAGS) + +.PHONY: test +test: + $(TEST_COMMAND) + +.PHONY: bench +bench: + go test -bench=. + +FUZZ_TIME ?= 10s + +# see https://github.com/golang/go/issues/46312 +# and https://stackoverflow.com/a/72673487/4867444 +# if we end up having more fuzz tests +.PHONY: test_with_fuzz +test_with_fuzz: + $(TEST_COMMAND) -fuzz=FuzzRoundTripJSON -fuzztime=$(FUZZ_TIME) + $(TEST_COMMAND) -fuzz=FuzzRoundTripYAML -fuzztime=$(FUZZ_TIME) + +.PHONY: fuzz +fuzz: test_with_fuzz + +.PHONY: lint +lint: + golangci-lint run diff --git a/vendor/github.com/wk8/go-ordered-map/v2/README.md b/vendor/github.com/wk8/go-ordered-map/v2/README.md new file mode 100644 index 000000000..ebd8a4d37 --- /dev/null +++ b/vendor/github.com/wk8/go-ordered-map/v2/README.md @@ -0,0 +1,207 @@ +[![Go Reference](https://pkg.go.dev/badge/github.com/wk8/go-ordered-map/v2.svg)](https://pkg.go.dev/github.com/wk8/go-ordered-map/v2) +[![Build Status](https://circleci.com/gh/wk8/go-ordered-map.svg?style=svg)](https://app.circleci.com/pipelines/github/wk8/go-ordered-map) + +# Golang Ordered Maps + +Same as regular maps, but also remembers the order in which keys were inserted, akin to [Python's `collections.OrderedDict`s](https://docs.python.org/3.7/library/collections.html#ordereddict-objects). + +It offers the following features: +* optimal runtime performance (all operations are constant time) +* optimal memory usage (only one copy of values, no unnecessary memory allocation) +* allows iterating from newest or oldest keys indifferently, without memory copy, allowing to `break` the iteration, and in time linear to the number of keys iterated over rather than the total length of the ordered map +* supports any generic types for both keys and values. If you're running go < 1.18, you can use [version 1](https://github.com/wk8/go-ordered-map/tree/v1) that takes and returns generic `interface{}`s instead of using generics +* idiomatic API, akin to that of [`container/list`](https://golang.org/pkg/container/list) +* support for JSON and YAML marshalling + +## Documentation + +[The full documentation is available on pkg.go.dev](https://pkg.go.dev/github.com/wk8/go-ordered-map/v2). + +## Installation +```bash +go get -u github.com/wk8/go-ordered-map/v2 +``` + +Or use your favorite golang vendoring tool! + +## Supported go versions + +Go >= 1.23 is required to use version >= 2.2.0 of this library, as it uses generics and iterators. + +if you're running go < 1.23, you can use [version 2.1.8](https://github.com/wk8/go-ordered-map/tree/v2.1.8) instead. + +If you're running go < 1.18, you can use [version 1](https://github.com/wk8/go-ordered-map/tree/v1) instead. + +## Example / usage + +```go +package main + +import ( + "fmt" + + "github.com/wk8/go-ordered-map/v2" +) + +func main() { + om := orderedmap.New[string, string]() + + om.Set("foo", "bar") + om.Set("bar", "baz") + om.Set("coucou", "toi") + + fmt.Println(om.Get("foo")) // => "bar", true + fmt.Println(om.Get("i dont exist")) // => "", false + + // iterating pairs from oldest to newest: + for pair := om.Oldest(); pair != nil; pair = pair.Next() { + fmt.Printf("%s => %s\n", pair.Key, pair.Value) + } // prints: + // foo => bar + // bar => baz + // coucou => toi + + // iterating over the 2 newest pairs: + i := 0 + for pair := om.Newest(); pair != nil; pair = pair.Prev() { + fmt.Printf("%s => %s\n", pair.Key, pair.Value) + i++ + if i >= 2 { + break + } + } // prints: + // coucou => toi + // bar => baz +} +``` + +An `OrderedMap`'s keys must implement `comparable`, and its values can be anything, for example: + +```go +type myStruct struct { + payload string +} + +func main() { + om := orderedmap.New[int, *myStruct]() + + om.Set(12, &myStruct{"foo"}) + om.Set(1, &myStruct{"bar"}) + + value, present := om.Get(12) + if !present { + panic("should be there!") + } + fmt.Println(value.payload) // => foo + + for pair := om.Oldest(); pair != nil; pair = pair.Next() { + fmt.Printf("%d => %s\n", pair.Key, pair.Value.payload) + } // prints: + // 12 => foo + // 1 => bar +} +``` + +Also worth noting that you can provision ordered maps with a capacity hint, as you would do by passing an optional hint to `make(map[K]V, capacity`): +```go +om := orderedmap.New[int, *myStruct](28) +``` + +You can also pass in some initial data to store in the map: +```go +om := orderedmap.New[int, string](orderedmap.WithInitialData[int, string]( + orderedmap.Pair[int, string]{ + Key: 12, + Value: "foo", + }, + orderedmap.Pair[int, string]{ + Key: 28, + Value: "bar", + }, +)) +``` + +`OrderedMap`s also support JSON serialization/deserialization, and preserves order: + +```go +// serialization +data, err := json.Marshal(om) +... + +// deserialization +om := orderedmap.New[string, string]() // or orderedmap.New[int, any](), or any type you expect +err := json.Unmarshal(data, &om) +... +``` + +Similarly, it also supports YAML serialization/deserialization using the yaml.v3 package, which also preserves order: + +```go +// serialization +data, err := yaml.Marshal(om) +... + +// deserialization +om := orderedmap.New[string, string]() // or orderedmap.New[int, any](), or any type you expect +err := yaml.Unmarshal(data, &om) +... +``` + +## Iterator support (go >= 1.23) + +The `FromOldest`, `FromNewest`, `KeysFromOldest`, `KeysFromNewest`, `ValuesFromOldest` and `ValuesFromNewest` methods return iterators over the map's pairs, starting from the oldest or newest pair, respectively. + +For example: + +```go +om := orderedmap.New[int, string]() +om.Set(1, "foo") +om.Set(2, "bar") +om.Set(3, "baz") + +for k, v := range om.FromOldest() { + fmt.Printf("%d => %s\n", k, v) +} + +// prints: +// 1 => foo +// 2 => bar +// 3 => baz + +for k := range om.KeysNewest() { + fmt.Printf("%d\n", k) +} + +// prints: +// 3 +// 2 +// 1 +``` + +`From` is a convenience function that creates a new `OrderedMap` from an iterator over key-value pairs. + +```go +om := orderedmap.New[int, string]() +om.Set(1, "foo") +om.Set(2, "bar") +om.Set(3, "baz") + +om2 := orderedmap.From(om.FromOldest()) + +for k, v := range om2.FromOldest() { + fmt.Printf("%d => %s\n", k, v) +} + +// prints: +// 1 => foo +// 2 => bar +// 3 => baz +``` + +## Alternatives + +There are several other ordered map golang implementations out there, but I believe that at the time of writing none of them offer the same functionality as this library; more specifically: +* [iancoleman/orderedmap](https://github.com/iancoleman/orderedmap) only accepts `string` keys, its `Delete` operations are linear +* [cevaris/ordered_map](https://github.com/cevaris/ordered_map) uses a channel for iterations, and leaks goroutines if the iteration is interrupted before fully traversing the map +* [mantyr/iterator](https://github.com/mantyr/iterator) also uses a channel for iterations, and its `Delete` operations are linear +* [samdolan/go-ordered-map](https://github.com/samdolan/go-ordered-map) adds unnecessary locking (users should add their own locking instead if they need it), its `Delete` and `Get` operations are linear, iterations trigger a linear memory allocation diff --git a/vendor/github.com/wk8/go-ordered-map/v2/json.go b/vendor/github.com/wk8/go-ordered-map/v2/json.go new file mode 100644 index 000000000..53f176a05 --- /dev/null +++ b/vendor/github.com/wk8/go-ordered-map/v2/json.go @@ -0,0 +1,182 @@ +package orderedmap + +import ( + "bytes" + "encoding" + "encoding/json" + "fmt" + "reflect" + "unicode/utf8" + + "github.com/buger/jsonparser" + "github.com/mailru/easyjson/jwriter" +) + +var ( + _ json.Marshaler = &OrderedMap[int, any]{} + _ json.Unmarshaler = &OrderedMap[int, any]{} +) + +// MarshalJSON implements the json.Marshaler interface. +func (om *OrderedMap[K, V]) MarshalJSON() ([]byte, error) { //nolint:funlen + if om == nil || om.list == nil { + return []byte("null"), nil + } + + writer := jwriter.Writer{} + writer.RawByte('{') + + for pair, firstIteration := om.Oldest(), true; pair != nil; pair = pair.Next() { + if firstIteration { + firstIteration = false + } else { + writer.RawByte(',') + } + + switch key := any(pair.Key).(type) { + case string: + writer.String(key) + case encoding.TextMarshaler: + writer.RawByte('"') + writer.Raw(key.MarshalText()) + writer.RawByte('"') + case int: + writer.IntStr(key) + case int8: + writer.Int8Str(key) + case int16: + writer.Int16Str(key) + case int32: + writer.Int32Str(key) + case int64: + writer.Int64Str(key) + case uint: + writer.UintStr(key) + case uint8: + writer.Uint8Str(key) + case uint16: + writer.Uint16Str(key) + case uint32: + writer.Uint32Str(key) + case uint64: + writer.Uint64Str(key) + default: + + // this switch takes care of wrapper types around primitive types, such as + // type myType string + switch keyValue := reflect.ValueOf(key); keyValue.Type().Kind() { + case reflect.String: + writer.String(keyValue.String()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + writer.Int64Str(keyValue.Int()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + writer.Uint64Str(keyValue.Uint()) + default: + return nil, fmt.Errorf("unsupported key type: %T", key) + } + } + + writer.RawByte(':') + // the error is checked at the end of the function + writer.Raw(json.Marshal(pair.Value)) + } + + writer.RawByte('}') + + return dumpWriter(&writer) +} + +func dumpWriter(writer *jwriter.Writer) ([]byte, error) { + if writer.Error != nil { + return nil, writer.Error + } + + var buf bytes.Buffer + buf.Grow(writer.Size()) + if _, err := writer.DumpTo(&buf); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (om *OrderedMap[K, V]) UnmarshalJSON(data []byte) error { + if om.list == nil { + om.initialize(0) + } + + return jsonparser.ObjectEach( + data, + func(keyData []byte, valueData []byte, dataType jsonparser.ValueType, offset int) error { + if dataType == jsonparser.String { + // jsonparser removes the enclosing quotes; we need to restore them to make a valid JSON + valueData = data[offset-len(valueData)-2 : offset] + } + + var key K + var value V + + switch typedKey := any(&key).(type) { + case *string: + s, err := decodeUTF8(keyData) + if err != nil { + return err + } + *typedKey = s + case encoding.TextUnmarshaler: + if err := typedKey.UnmarshalText(keyData); err != nil { + return err + } + case *int, *int8, *int16, *int32, *int64, *uint, *uint8, *uint16, *uint32, *uint64: + if err := json.Unmarshal(keyData, typedKey); err != nil { + return err + } + default: + // this switch takes care of wrapper types around primitive types, such as + // type myType string + switch reflect.TypeOf(key).Kind() { + case reflect.String: + s, err := decodeUTF8(keyData) + if err != nil { + return err + } + + convertedKeyData := reflect.ValueOf(s).Convert(reflect.TypeOf(key)) + reflect.ValueOf(&key).Elem().Set(convertedKeyData) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if err := json.Unmarshal(keyData, &key); err != nil { + return err + } + default: + return fmt.Errorf("unsupported key type: %T", key) + } + } + + if err := json.Unmarshal(valueData, &value); err != nil { + return err + } + + om.Set(key, value) + return nil + }) +} + +func decodeUTF8(input []byte) (string, error) { + remaining, offset := input, 0 + runes := make([]rune, 0, len(remaining)) + + for len(remaining) > 0 { + r, size := utf8.DecodeRune(remaining) + if r == utf8.RuneError && size <= 1 { + return "", fmt.Errorf("not a valid UTF-8 string (at position %d): %s", offset, string(input)) + } + + runes = append(runes, r) + remaining = remaining[size:] + offset += size + } + + return string(runes), nil +} diff --git a/vendor/github.com/wk8/go-ordered-map/v2/orderedmap.go b/vendor/github.com/wk8/go-ordered-map/v2/orderedmap.go new file mode 100644 index 000000000..45bf8622d --- /dev/null +++ b/vendor/github.com/wk8/go-ordered-map/v2/orderedmap.go @@ -0,0 +1,373 @@ +// Package orderedmap implements an ordered map, i.e. a map that also keeps track of +// the order in which keys were inserted. +// +// All operations are constant-time. +// +// Github repo: https://github.com/wk8/go-ordered-map +package orderedmap + +import ( + "fmt" + "iter" + + list "github.com/bahlo/generic-list-go" +) + +type Pair[K comparable, V any] struct { + Key K + Value V + + element *list.Element[*Pair[K, V]] +} + +type OrderedMap[K comparable, V any] struct { + pairs map[K]*Pair[K, V] + list *list.List[*Pair[K, V]] +} + +type initConfig[K comparable, V any] struct { + capacity int + initialData []Pair[K, V] +} + +type InitOption[K comparable, V any] func(config *initConfig[K, V]) + +// WithCapacity allows giving a capacity hint for the map, akin to the standard make(map[K]V, capacity). +func WithCapacity[K comparable, V any](capacity int) InitOption[K, V] { + return func(c *initConfig[K, V]) { + c.capacity = capacity + } +} + +// WithInitialData allows passing in initial data for the map. +func WithInitialData[K comparable, V any](initialData ...Pair[K, V]) InitOption[K, V] { + return func(c *initConfig[K, V]) { + c.initialData = initialData + if c.capacity < len(initialData) { + c.capacity = len(initialData) + } + } +} + +// New creates a new OrderedMap. +// options can either be one or several InitOption[K, V], or a single integer, +// which is then interpreted as a capacity hint, à la make(map[K]V, capacity). +func New[K comparable, V any](options ...any) *OrderedMap[K, V] { + orderedMap := &OrderedMap[K, V]{} + + var config initConfig[K, V] + for _, untypedOption := range options { + switch option := untypedOption.(type) { + case int: + if len(options) != 1 { + invalidOption() + } + config.capacity = option + + case InitOption[K, V]: + option(&config) + + default: + invalidOption() + } + } + + orderedMap.initialize(config.capacity) + orderedMap.AddPairs(config.initialData...) + + return orderedMap +} + +const invalidOptionMessage = `when using orderedmap.New[K,V]() with options, either provide one or several InitOption[K, V]; or a single integer which is then interpreted as a capacity hint, à la make(map[K]V, capacity).` //nolint:lll + +func invalidOption() { panic(invalidOptionMessage) } + +func (om *OrderedMap[K, V]) initialize(capacity int) { + om.pairs = make(map[K]*Pair[K, V], capacity) + om.list = list.New[*Pair[K, V]]() +} + +// Get looks for the given key, and returns the value associated with it, +// or V's nil value if not found. The boolean it returns says whether the key is present in the map. +func (om *OrderedMap[K, V]) Get(key K) (val V, present bool) { + if pair, present := om.pairs[key]; present { + return pair.Value, true + } + + return +} + +// Load is an alias for Get, mostly to present an API similar to `sync.Map`'s. +func (om *OrderedMap[K, V]) Load(key K) (V, bool) { + return om.Get(key) +} + +// Value returns the value associated with the given key or the zero value. +func (om *OrderedMap[K, V]) Value(key K) (val V) { + if pair, present := om.pairs[key]; present { + val = pair.Value + } + return +} + +// GetPair looks for the given key, and returns the pair associated with it, +// or nil if not found. The Pair struct can then be used to iterate over the ordered map +// from that point, either forward or backward. +func (om *OrderedMap[K, V]) GetPair(key K) *Pair[K, V] { + return om.pairs[key] +} + +// Set sets the key-value pair, and returns what `Get` would have returned +// on that key prior to the call to `Set`. +func (om *OrderedMap[K, V]) Set(key K, value V) (val V, present bool) { + if pair, present := om.pairs[key]; present { + oldValue := pair.Value + pair.Value = value + return oldValue, true + } + + pair := &Pair[K, V]{ + Key: key, + Value: value, + } + pair.element = om.list.PushBack(pair) + om.pairs[key] = pair + + return +} + +// AddPairs allows setting multiple pairs at a time. It's equivalent to calling +// Set on each pair sequentially. +func (om *OrderedMap[K, V]) AddPairs(pairs ...Pair[K, V]) { + for _, pair := range pairs { + om.Set(pair.Key, pair.Value) + } +} + +// Store is an alias for Set, mostly to present an API similar to `sync.Map`'s. +func (om *OrderedMap[K, V]) Store(key K, value V) (V, bool) { + return om.Set(key, value) +} + +// Delete removes the key-value pair, and returns what `Get` would have returned +// on that key prior to the call to `Delete`. +func (om *OrderedMap[K, V]) Delete(key K) (val V, present bool) { + if pair, present := om.pairs[key]; present { + om.list.Remove(pair.element) + delete(om.pairs, key) + return pair.Value, true + } + return +} + +// Len returns the length of the ordered map. +func (om *OrderedMap[K, V]) Len() int { + if om == nil || om.pairs == nil { + return 0 + } + return len(om.pairs) +} + +// Oldest returns a pointer to the oldest pair. It's meant to be used to iterate on the ordered map's +// pairs from the oldest to the newest, e.g.: +// for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() { fmt.Printf("%v => %v\n", pair.Key, pair.Value) } +func (om *OrderedMap[K, V]) Oldest() *Pair[K, V] { + if om == nil || om.list == nil { + return nil + } + return listElementToPair(om.list.Front()) +} + +// Newest returns a pointer to the newest pair. It's meant to be used to iterate on the ordered map's +// pairs from the newest to the oldest, e.g.: +// for pair := orderedMap.Newest(); pair != nil; pair = pair.Prev() { fmt.Printf("%v => %v\n", pair.Key, pair.Value) } +func (om *OrderedMap[K, V]) Newest() *Pair[K, V] { + if om == nil || om.list == nil { + return nil + } + return listElementToPair(om.list.Back()) +} + +// Next returns a pointer to the next pair. +func (p *Pair[K, V]) Next() *Pair[K, V] { + return listElementToPair(p.element.Next()) +} + +// Prev returns a pointer to the previous pair. +func (p *Pair[K, V]) Prev() *Pair[K, V] { + return listElementToPair(p.element.Prev()) +} + +func listElementToPair[K comparable, V any](element *list.Element[*Pair[K, V]]) *Pair[K, V] { + if element == nil { + return nil + } + return element.Value +} + +// KeyNotFoundError may be returned by functions in this package when they're called with keys that are not present +// in the map. +type KeyNotFoundError[K comparable] struct { + MissingKey K +} + +func (e *KeyNotFoundError[K]) Error() string { + return fmt.Sprintf("missing key: %v", e.MissingKey) +} + +// MoveAfter moves the value associated with key to its new position after the one associated with markKey. +// Returns an error iff key or markKey are not present in the map. If an error is returned, +// it will be a KeyNotFoundError. +func (om *OrderedMap[K, V]) MoveAfter(key, markKey K) error { + elements, err := om.getElements(key, markKey) + if err != nil { + return err + } + om.list.MoveAfter(elements[0], elements[1]) + return nil +} + +// MoveBefore moves the value associated with key to its new position before the one associated with markKey. +// Returns an error iff key or markKey are not present in the map. If an error is returned, +// it will be a KeyNotFoundError. +func (om *OrderedMap[K, V]) MoveBefore(key, markKey K) error { + elements, err := om.getElements(key, markKey) + if err != nil { + return err + } + om.list.MoveBefore(elements[0], elements[1]) + return nil +} + +func (om *OrderedMap[K, V]) getElements(keys ...K) ([]*list.Element[*Pair[K, V]], error) { + elements := make([]*list.Element[*Pair[K, V]], len(keys)) + for i, k := range keys { + pair, present := om.pairs[k] + if !present { + return nil, &KeyNotFoundError[K]{k} + } + elements[i] = pair.element + } + return elements, nil +} + +// MoveToBack moves the value associated with key to the back of the ordered map, +// i.e. makes it the newest pair in the map. +// Returns an error iff key is not present in the map. If an error is returned, +// it will be a KeyNotFoundError. +func (om *OrderedMap[K, V]) MoveToBack(key K) error { + _, err := om.GetAndMoveToBack(key) + return err +} + +// MoveToFront moves the value associated with key to the front of the ordered map, +// i.e. makes it the oldest pair in the map. +// Returns an error iff key is not present in the map. If an error is returned, +// it will be a KeyNotFoundError. +func (om *OrderedMap[K, V]) MoveToFront(key K) error { + _, err := om.GetAndMoveToFront(key) + return err +} + +// GetAndMoveToBack combines Get and MoveToBack in the same call. If an error is returned, +// it will be a KeyNotFoundError. +func (om *OrderedMap[K, V]) GetAndMoveToBack(key K) (val V, err error) { + if pair, present := om.pairs[key]; present { + val = pair.Value + om.list.MoveToBack(pair.element) + } else { + err = &KeyNotFoundError[K]{key} + } + + return +} + +// GetAndMoveToFront combines Get and MoveToFront in the same call. If an error is returned, +// it will be a KeyNotFoundError. +func (om *OrderedMap[K, V]) GetAndMoveToFront(key K) (val V, err error) { + if pair, present := om.pairs[key]; present { + val = pair.Value + om.list.MoveToFront(pair.element) + } else { + err = &KeyNotFoundError[K]{key} + } + + return +} + +// FromOldest returns an iterator over all the key-value pairs in the map, starting from the oldest pair. +func (om *OrderedMap[K, V]) FromOldest() iter.Seq2[K, V] { + return func(yield func(K, V) bool) { + for pair := om.Oldest(); pair != nil; pair = pair.Next() { + if !yield(pair.Key, pair.Value) { + return + } + } + } +} + +// FromNewest returns an iterator over all the key-value pairs in the map, starting from the newest pair. +func (om *OrderedMap[K, V]) FromNewest() iter.Seq2[K, V] { + return func(yield func(K, V) bool) { + for pair := om.Newest(); pair != nil; pair = pair.Prev() { + if !yield(pair.Key, pair.Value) { + return + } + } + } +} + +// KeysFromOldest returns an iterator over all the keys in the map, starting from the oldest pair. +func (om *OrderedMap[K, V]) KeysFromOldest() iter.Seq[K] { + return func(yield func(K) bool) { + for pair := om.Oldest(); pair != nil; pair = pair.Next() { + if !yield(pair.Key) { + return + } + } + } +} + +// KeysFromNewest returns an iterator over all the keys in the map, starting from the newest pair. +func (om *OrderedMap[K, V]) KeysFromNewest() iter.Seq[K] { + return func(yield func(K) bool) { + for pair := om.Newest(); pair != nil; pair = pair.Prev() { + if !yield(pair.Key) { + return + } + } + } +} + +// ValuesFromOldest returns an iterator over all the values in the map, starting from the oldest pair. +func (om *OrderedMap[K, V]) ValuesFromOldest() iter.Seq[V] { + return func(yield func(V) bool) { + for pair := om.Oldest(); pair != nil; pair = pair.Next() { + if !yield(pair.Value) { + return + } + } + } +} + +// ValuesFromNewest returns an iterator over all the values in the map, starting from the newest pair. +func (om *OrderedMap[K, V]) ValuesFromNewest() iter.Seq[V] { + return func(yield func(V) bool) { + for pair := om.Newest(); pair != nil; pair = pair.Prev() { + if !yield(pair.Value) { + return + } + } + } +} + +// From creates a new OrderedMap from an iterator over key-value pairs. +func From[K comparable, V any](i iter.Seq2[K, V]) *OrderedMap[K, V] { + oMap := New[K, V]() + + for k, v := range i { + oMap.Set(k, v) + } + + return oMap +} diff --git a/vendor/github.com/wk8/go-ordered-map/v2/yaml.go b/vendor/github.com/wk8/go-ordered-map/v2/yaml.go new file mode 100644 index 000000000..602247128 --- /dev/null +++ b/vendor/github.com/wk8/go-ordered-map/v2/yaml.go @@ -0,0 +1,71 @@ +package orderedmap + +import ( + "fmt" + + "gopkg.in/yaml.v3" +) + +var ( + _ yaml.Marshaler = &OrderedMap[int, any]{} + _ yaml.Unmarshaler = &OrderedMap[int, any]{} +) + +// MarshalYAML implements the yaml.Marshaler interface. +func (om *OrderedMap[K, V]) MarshalYAML() (interface{}, error) { + if om == nil { + return []byte("null"), nil + } + + node := yaml.Node{ + Kind: yaml.MappingNode, + } + + for pair := om.Oldest(); pair != nil; pair = pair.Next() { + key, value := pair.Key, pair.Value + + keyNode := &yaml.Node{} + + // serialize key to yaml, then deserialize it back into the node + // this is a hack to get the correct tag for the key + if err := keyNode.Encode(key); err != nil { + return nil, err + } + + valueNode := &yaml.Node{} + if err := valueNode.Encode(value); err != nil { + return nil, err + } + + node.Content = append(node.Content, keyNode, valueNode) + } + + return &node, nil +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (om *OrderedMap[K, V]) UnmarshalYAML(value *yaml.Node) error { + if value.Kind != yaml.MappingNode { + return fmt.Errorf("pipeline must contain YAML mapping, has %v", value.Kind) + } + + if om.list == nil { + om.initialize(0) + } + + for index := 0; index < len(value.Content); index += 2 { + var key K + var val V + + if err := value.Content[index].Decode(&key); err != nil { + return err + } + if err := value.Content[index+1].Decode(&val); err != nil { + return err + } + + om.Set(key, val) + } + + return nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index eb965e0cb..246e90c7d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -91,6 +91,9 @@ github.com/asaskevich/govalidator ## explicit github.com/aymerick/douceur/css github.com/aymerick/douceur/parser +# github.com/bahlo/generic-list-go v0.2.0 +## explicit; go 1.18 +github.com/bahlo/generic-list-go # github.com/beorn7/perks v1.0.1 ## explicit; go 1.11 github.com/beorn7/perks/quantile @@ -98,6 +101,9 @@ github.com/beorn7/perks/quantile ## explicit; go 1.14 github.com/buckket/go-blurhash github.com/buckket/go-blurhash/base83 +# github.com/buger/jsonparser v1.1.1 +## explicit; go 1.13 +github.com/buger/jsonparser # github.com/bytedance/sonic v1.11.6 ## explicit; go 1.16 github.com/bytedance/sonic @@ -919,8 +925,8 @@ github.com/ugorji/go/codec github.com/ulule/limiter/v3 github.com/ulule/limiter/v3/drivers/store/common github.com/ulule/limiter/v3/drivers/store/memory -# github.com/uptrace/bun v1.2.5 -## explicit; go 1.22 +# github.com/uptrace/bun v1.2.6 +## explicit; go 1.23 github.com/uptrace/bun github.com/uptrace/bun/dialect github.com/uptrace/bun/dialect/feature @@ -930,15 +936,16 @@ github.com/uptrace/bun/internal github.com/uptrace/bun/internal/parser github.com/uptrace/bun/internal/tagparser github.com/uptrace/bun/migrate +github.com/uptrace/bun/migrate/sqlschema github.com/uptrace/bun/schema -# github.com/uptrace/bun/dialect/pgdialect v1.2.5 -## explicit; go 1.22 +# github.com/uptrace/bun/dialect/pgdialect v1.2.6 +## explicit; go 1.23 github.com/uptrace/bun/dialect/pgdialect -# github.com/uptrace/bun/dialect/sqlitedialect v1.2.5 -## explicit; go 1.22 +# github.com/uptrace/bun/dialect/sqlitedialect v1.2.6 +## explicit; go 1.23 github.com/uptrace/bun/dialect/sqlitedialect -# github.com/uptrace/bun/extra/bunotel v1.2.5 -## explicit; go 1.22 +# github.com/uptrace/bun/extra/bunotel v1.2.6 +## explicit; go 1.23 github.com/uptrace/bun/extra/bunotel # github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 ## explicit; go 1.22 @@ -955,6 +962,9 @@ github.com/vmihailenco/tagparser/v2/internal/parser # github.com/wagslane/go-password-validator v0.3.0 ## explicit; go 1.16 github.com/wagslane/go-password-validator +# github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41 +## explicit; go 1.23 +github.com/wk8/go-ordered-map/v2 # github.com/yuin/goldmark v1.7.8 ## explicit; go 1.19 github.com/yuin/goldmark From 5c818debb24218a5f876082482d67fb9238eae10 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Mon, 25 Nov 2024 18:21:19 +0100 Subject: [PATCH 09/26] [chore] Sign the bloody thing, fix the other bloody thing (#3572) --- .drone.yml | 2 +- .goreleaser.yml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.drone.yml b/.drone.yml index 5b954876d..cd56a9603 100644 --- a/.drone.yml +++ b/.drone.yml @@ -223,6 +223,6 @@ steps: --- kind: signature -hmac: 9810bf692fb1029c13b0a1e2f556e2306d16f7d3eec9ca6163a0499c147280c1 +hmac: c79f1c3b16db8da7e3b01b960021a583ec81069aff8afd4425f049dd140f0620 ... diff --git a/.goreleaser.yml b/.goreleaser.yml index c1c170c5a..91e6afca9 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,7 +1,6 @@ # Version 2 of GoReleaser: https://goreleaser.com/errors/version/ version: 2 project_name: gotosocial -version: 2 # https://goreleaser.com/customization/hooks/ before: From 6a8af426474acd1ffd5cb3265a2372003977326a Mon Sep 17 00:00:00 2001 From: Vyr Cossont Date: Tue, 26 Nov 2024 08:23:00 -0800 Subject: [PATCH 10/26] [bugfix] Allow unsetting filter expiration dates (#3560) * Regression tests for #3497 (v1 and v2) * use Nullable type for v2 form.expires_in --------- Co-authored-by: tobi --- .../api/client/filters/v1/filterpost_test.go | 48 ++++++-- .../api/client/filters/v1/filterput_test.go | 75 ++++++++++-- internal/api/client/filters/v1/validate.go | 14 +-- internal/api/client/filters/v2/filterpost.go | 14 +-- .../api/client/filters/v2/filterpost_test.go | 49 ++++++-- internal/api/client/filters/v2/filterput.go | 14 +-- .../api/client/filters/v2/filterput_test.go | 82 ++++++++++++-- internal/api/model/filterv1.go | 2 +- internal/api/model/filterv2.go | 4 +- internal/api/model/nullable.go | 107 ++++++++++++++++++ internal/api/util/parseform.go | 36 ++++-- internal/processing/filters/v1/create.go | 2 +- internal/processing/filters/v1/update.go | 2 +- internal/processing/filters/v2/create.go | 2 +- internal/processing/filters/v2/update.go | 13 ++- 15 files changed, 379 insertions(+), 85 deletions(-) create mode 100644 internal/api/model/nullable.go diff --git a/internal/api/client/filters/v1/filterpost_test.go b/internal/api/client/filters/v1/filterpost_test.go index 2b18abf13..b7aecc573 100644 --- a/internal/api/client/filters/v1/filterpost_test.go +++ b/internal/api/client/filters/v1/filterpost_test.go @@ -41,6 +41,7 @@ func (suite *FiltersTestSuite) postFilter( irreversible *bool, wholeWord *bool, expiresIn *int, + expiresInStr *string, requestJson *string, expectedHTTPStatus int, expectedBody string, @@ -75,6 +76,8 @@ func (suite *FiltersTestSuite) postFilter( } if expiresIn != nil { ctx.Request.Form["expires_in"] = []string{strconv.Itoa(*expiresIn)} + } else if expiresInStr != nil { + ctx.Request.Form["expires_in"] = []string{*expiresInStr} } } @@ -124,7 +127,7 @@ func (suite *FiltersTestSuite) TestPostFilterFull() { irreversible := false wholeWord := true expiresIn := 86400 - filter, err := suite.postFilter(&phrase, &context, &irreversible, &wholeWord, &expiresIn, nil, http.StatusOK, "") + filter, err := suite.postFilter(&phrase, &context, &irreversible, &wholeWord, &expiresIn, nil, nil, http.StatusOK, "") if err != nil { suite.FailNow(err.Error()) } @@ -155,7 +158,7 @@ func (suite *FiltersTestSuite) TestPostFilterFullJSON() { "whole_word": true, "expires_in": 86400.1 }` - filter, err := suite.postFilter(nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "") + filter, err := suite.postFilter(nil, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "") if err != nil { suite.FailNow(err.Error()) } @@ -182,7 +185,7 @@ func (suite *FiltersTestSuite) TestPostFilterMinimal() { phrase := "GNU/Linux" context := []string{"home"} - filter, err := suite.postFilter(&phrase, &context, nil, nil, nil, nil, http.StatusOK, "") + filter, err := suite.postFilter(&phrase, &context, nil, nil, nil, nil, nil, http.StatusOK, "") if err != nil { suite.FailNow(err.Error()) } @@ -203,7 +206,7 @@ func (suite *FiltersTestSuite) TestPostFilterMinimal() { func (suite *FiltersTestSuite) TestPostFilterEmptyPhrase() { phrase := "" context := []string{"home"} - _, err := suite.postFilter(&phrase, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") + _, err := suite.postFilter(&phrase, &context, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") if err != nil { suite.FailNow(err.Error()) } @@ -211,7 +214,7 @@ func (suite *FiltersTestSuite) TestPostFilterEmptyPhrase() { func (suite *FiltersTestSuite) TestPostFilterMissingPhrase() { context := []string{"home"} - _, err := suite.postFilter(nil, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") + _, err := suite.postFilter(nil, &context, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") if err != nil { suite.FailNow(err.Error()) } @@ -220,7 +223,7 @@ func (suite *FiltersTestSuite) TestPostFilterMissingPhrase() { func (suite *FiltersTestSuite) TestPostFilterEmptyContext() { phrase := "GNU/Linux" context := []string{} - _, err := suite.postFilter(&phrase, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") + _, err := suite.postFilter(&phrase, &context, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") if err != nil { suite.FailNow(err.Error()) } @@ -228,7 +231,7 @@ func (suite *FiltersTestSuite) TestPostFilterEmptyContext() { func (suite *FiltersTestSuite) TestPostFilterMissingContext() { phrase := "GNU/Linux" - _, err := suite.postFilter(&phrase, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") + _, err := suite.postFilter(&phrase, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") if err != nil { suite.FailNow(err.Error()) } @@ -237,8 +240,37 @@ func (suite *FiltersTestSuite) TestPostFilterMissingContext() { // There should be a filter with this phrase as its title in our test fixtures. Creating another should fail. func (suite *FiltersTestSuite) TestPostFilterTitleConflict() { phrase := "fnord" - _, err := suite.postFilter(&phrase, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") + _, err := suite.postFilter(&phrase, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") if err != nil { suite.FailNow(err.Error()) } } + +// postFilterWithExpiration creates a filter with optional expiration. +func (suite *FiltersTestSuite) postFilterWithExpiration(phrase *string, expiresIn *int, expiresInStr *string, requestJson *string) *apimodel.FilterV1 { + context := []string{"home"} + filter, err := suite.postFilter(phrase, &context, nil, nil, expiresIn, expiresInStr, requestJson, http.StatusOK, "") + if err != nil { + suite.FailNow(err.Error()) + } + return filter +} + +// Regression test for https://github.com/superseriousbusiness/gotosocial/issues/3497 +func (suite *FiltersTestSuite) TestPostFilterWithEmptyStringExpiration() { + title := "Form Sins" + expiresInStr := "" + filter := suite.postFilterWithExpiration(&title, nil, &expiresInStr, nil) + suite.Nil(filter.ExpiresAt) +} + +// Regression test related to https://github.com/superseriousbusiness/gotosocial/issues/3497 +func (suite *FiltersTestSuite) TestPostFilterWithNullExpirationJSON() { + requestJson := `{ + "phrase": "JSON Sins", + "context": ["home"], + "expires_in": null + }` + filter := suite.postFilterWithExpiration(nil, nil, nil, &requestJson) + suite.Nil(filter.ExpiresAt) +} diff --git a/internal/api/client/filters/v1/filterput_test.go b/internal/api/client/filters/v1/filterput_test.go index 40b52ee43..626bd52eb 100644 --- a/internal/api/client/filters/v1/filterput_test.go +++ b/internal/api/client/filters/v1/filterput_test.go @@ -42,6 +42,7 @@ func (suite *FiltersTestSuite) putFilter( irreversible *bool, wholeWord *bool, expiresIn *int, + expiresInStr *string, requestJson *string, expectedHTTPStatus int, expectedBody string, @@ -76,6 +77,8 @@ func (suite *FiltersTestSuite) putFilter( } if expiresIn != nil { ctx.Request.Form["expires_in"] = []string{strconv.Itoa(*expiresIn)} + } else if expiresInStr != nil { + ctx.Request.Form["expires_in"] = []string{*expiresInStr} } } @@ -128,7 +131,7 @@ func (suite *FiltersTestSuite) TestPutFilterFull() { irreversible := false wholeWord := true expiresIn := 86400 - filter, err := suite.putFilter(id, &phrase, &context, &irreversible, &wholeWord, &expiresIn, nil, http.StatusOK, "") + filter, err := suite.putFilter(id, &phrase, &context, &irreversible, &wholeWord, &expiresIn, nil, nil, http.StatusOK, "") if err != nil { suite.FailNow(err.Error()) } @@ -160,7 +163,7 @@ func (suite *FiltersTestSuite) TestPutFilterFullJSON() { "whole_word": true, "expires_in": 86400.1 }` - filter, err := suite.putFilter(id, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "") + filter, err := suite.putFilter(id, nil, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "") if err != nil { suite.FailNow(err.Error()) } @@ -188,7 +191,7 @@ func (suite *FiltersTestSuite) TestPutFilterMinimal() { id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID phrase := "GNU/Linux" context := []string{"home"} - filter, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, http.StatusOK, "") + filter, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, http.StatusOK, "") if err != nil { suite.FailNow(err.Error()) } @@ -210,7 +213,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyPhrase() { id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID phrase := "" context := []string{"home"} - _, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") + _, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") if err != nil { suite.FailNow(err.Error()) } @@ -219,7 +222,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyPhrase() { func (suite *FiltersTestSuite) TestPutFilterMissingPhrase() { id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID context := []string{"home"} - _, err := suite.putFilter(id, nil, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") + _, err := suite.putFilter(id, nil, &context, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") if err != nil { suite.FailNow(err.Error()) } @@ -229,7 +232,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyContext() { id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID phrase := "GNU/Linux" context := []string{} - _, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") + _, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") if err != nil { suite.FailNow(err.Error()) } @@ -238,7 +241,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyContext() { func (suite *FiltersTestSuite) TestPutFilterMissingContext() { id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID phrase := "GNU/Linux" - _, err := suite.putFilter(id, &phrase, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") + _, err := suite.putFilter(id, &phrase, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") if err != nil { suite.FailNow(err.Error()) } @@ -248,7 +251,7 @@ func (suite *FiltersTestSuite) TestPutFilterMissingContext() { func (suite *FiltersTestSuite) TestPutFilterTitleConflict() { id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID phrase := "metasyntactic variables" - _, err := suite.putFilter(id, &phrase, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") + _, err := suite.putFilter(id, &phrase, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") if err != nil { suite.FailNow(err.Error()) } @@ -258,7 +261,7 @@ func (suite *FiltersTestSuite) TestPutAnotherAccountsFilter() { id := suite.testFilterKeywords["local_account_2_filter_1_keyword_1"].ID phrase := "GNU/Linux" context := []string{"home"} - _, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`) + _, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`) if err != nil { suite.FailNow(err.Error()) } @@ -268,8 +271,60 @@ func (suite *FiltersTestSuite) TestPutNonexistentFilter() { id := "not_even_a_real_ULID" phrase := "GNU/Linux" context := []string{"home"} - _, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`) + _, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`) if err != nil { suite.FailNow(err.Error()) } } + +// setFilterExpiration sets filter expiration. +func (suite *FiltersTestSuite) setFilterExpiration(id string, phrase *string, expiresIn *int, expiresInStr *string, requestJson *string) *apimodel.FilterV1 { + context := []string{"home"} + filter, err := suite.putFilter(id, phrase, &context, nil, nil, expiresIn, expiresInStr, requestJson, http.StatusOK, "") + if err != nil { + suite.FailNow(err.Error()) + } + return filter +} + +// Regression test for https://github.com/superseriousbusiness/gotosocial/issues/3497 +func (suite *FiltersTestSuite) TestPutFilterUnsetExpirationDateEmptyString() { + filterKeyword := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"] + id := filterKeyword.ID + phrase := filterKeyword.Keyword + + // Setup: set an expiration date for the filter. + expiresIn := 86400 + filter := suite.setFilterExpiration(id, &phrase, &expiresIn, nil, nil) + if !suite.NotNil(filter.ExpiresAt) { + suite.FailNow("Test precondition failed") + } + + // Unset the filter's expiration date by setting it to an empty string. + expiresInStr := "" + filter = suite.setFilterExpiration(id, &phrase, nil, &expiresInStr, nil) + suite.Nil(filter.ExpiresAt) +} + +// Regression test related to https://github.com/superseriousbusiness/gotosocial/issues/3497 +func (suite *FiltersTestSuite) TestPutFilterUnsetExpirationDateNullJSON() { + filterKeyword := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"] + id := filterKeyword.ID + phrase := filterKeyword.Keyword + + // Setup: set an expiration date for the filter. + expiresIn := 86400 + filter := suite.setFilterExpiration(id, &phrase, &expiresIn, nil, nil) + if !suite.NotNil(filter.ExpiresAt) { + suite.FailNow("Test precondition failed") + } + + // Unset the filter's expiration date by setting it to a null literal. + requestJson := `{ + "phrase": "fnord", + "context": ["home"], + "expires_in": null + }` + filter = suite.setFilterExpiration(id, nil, nil, nil, &requestJson) + suite.Nil(filter.ExpiresAt) +} diff --git a/internal/api/client/filters/v1/validate.go b/internal/api/client/filters/v1/validate.go index 9e876c8cf..9e31abb89 100644 --- a/internal/api/client/filters/v1/validate.go +++ b/internal/api/client/filters/v1/validate.go @@ -46,12 +46,11 @@ func validateNormalizeCreateUpdateFilter(form *apimodel.FilterCreateUpdateReques return errors.New("irreversible aka server-side drop filters are not supported yet") } - // Normalize filter expiry if necessary. - if form.ExpiresInI != nil { - // If we parsed this as JSON, expires_in - // may be either a float64 or a string. + // If `expires_in` was provided + // as JSON, then normalize it. + if form.ExpiresInI.IsSpecified() { var err error - form.ExpiresIn, err = apiutil.ParseDuration( + form.ExpiresIn, err = apiutil.ParseNullableDuration( form.ExpiresInI, "expires_in", ) @@ -60,10 +59,5 @@ func validateNormalizeCreateUpdateFilter(form *apimodel.FilterCreateUpdateReques } } - // Interpret zero as indefinite duration. - if form.ExpiresIn != nil && *form.ExpiresIn == 0 { - form.ExpiresIn = nil - } - return nil } diff --git a/internal/api/client/filters/v2/filterpost.go b/internal/api/client/filters/v2/filterpost.go index 632c4402f..ad6e83060 100644 --- a/internal/api/client/filters/v2/filterpost.go +++ b/internal/api/client/filters/v2/filterpost.go @@ -225,12 +225,11 @@ func validateNormalizeCreateFilter(form *apimodel.FilterCreateRequestV2) error { // Apply defaults for missing fields. form.FilterAction = util.Ptr(action) - // Normalize filter expiry if necessary. - if form.ExpiresInI != nil { - // If we parsed this as JSON, expires_in - // may be either a float64 or a string. + // If `expires_in` was provided + // as JSON, then normalize it. + if form.ExpiresInI.IsSpecified() { var err error - form.ExpiresIn, err = apiutil.ParseDuration( + form.ExpiresIn, err = apiutil.ParseNullableDuration( form.ExpiresInI, "expires_in", ) @@ -239,11 +238,6 @@ func validateNormalizeCreateFilter(form *apimodel.FilterCreateRequestV2) error { } } - // Interpret zero as indefinite duration. - if form.ExpiresIn != nil && *form.ExpiresIn == 0 { - form.ExpiresIn = nil - } - // Normalize and validate new keywords and statuses. for i, formKeyword := range form.Keywords { if err := validate.FilterKeyword(formKeyword.Keyword); err != nil { diff --git a/internal/api/client/filters/v2/filterpost_test.go b/internal/api/client/filters/v2/filterpost_test.go index 6e378874c..7a79f4665 100644 --- a/internal/api/client/filters/v2/filterpost_test.go +++ b/internal/api/client/filters/v2/filterpost_test.go @@ -36,7 +36,7 @@ import ( "github.com/superseriousbusiness/gotosocial/testrig" ) -func (suite *FiltersTestSuite) postFilter(title *string, context *[]string, action *string, expiresIn *int, keywordsAttributesKeyword *[]string, keywordsAttributesWholeWord *[]bool, statusesAttributesStatusID *[]string, requestJson *string, expectedHTTPStatus int, expectedBody string) (*apimodel.FilterV2, error) { +func (suite *FiltersTestSuite) postFilter(title *string, context *[]string, action *string, expiresIn *int, expiresInStr *string, keywordsAttributesWholeWord *[]bool, statusesAttributesStatusID *[]string, requestJson *string, expectedHTTPStatus int, expectedBody string, keywordsAttributesKeyword *[]string) (*apimodel.FilterV2, error) { // instantiate recorder + test context recorder := httptest.NewRecorder() ctx, _ := testrig.CreateGinTestContext(recorder, nil) @@ -64,6 +64,8 @@ func (suite *FiltersTestSuite) postFilter(title *string, context *[]string, acti } if expiresIn != nil { ctx.Request.Form["expires_in"] = []string{strconv.Itoa(*expiresIn)} + } else if expiresInStr != nil { + ctx.Request.Form["expires_in"] = []string{*expiresInStr} } if keywordsAttributesKeyword != nil { ctx.Request.Form["keywords_attributes[][keyword]"] = *keywordsAttributesKeyword @@ -130,7 +132,7 @@ func (suite *FiltersTestSuite) TestPostFilterFull() { keywordsAttributesWholeWord := []bool{true, false} // Checked in lexical order by status ID, so keep this sorted. statusAttributesStatusID := []string{"01HEN2QRFA8H3C6QPN7RD4KSR6", "01HEWV37MHV8BAC8ANFGVRRM5D"} - filter, err := suite.postFilter(&title, &context, &action, &expiresIn, &keywordsAttributesKeyword, &keywordsAttributesWholeWord, &statusAttributesStatusID, nil, http.StatusOK, "") + filter, err := suite.postFilter(&title, &context, &action, &expiresIn, nil, &keywordsAttributesWholeWord, &statusAttributesStatusID, nil, http.StatusOK, "", &keywordsAttributesKeyword) if err != nil { suite.FailNow(err.Error()) } @@ -197,7 +199,7 @@ func (suite *FiltersTestSuite) TestPostFilterFullJSON() { } ] }` - filter, err := suite.postFilter(nil, nil, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "") + filter, err := suite.postFilter(nil, nil, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "", nil) if err != nil { suite.FailNow(err.Error()) } @@ -245,7 +247,7 @@ func (suite *FiltersTestSuite) TestPostFilterMinimal() { title := "GNU/Linux" context := []string{"home"} - filter, err := suite.postFilter(&title, &context, nil, nil, nil, nil, nil, nil, http.StatusOK, "") + filter, err := suite.postFilter(&title, &context, nil, nil, nil, nil, nil, nil, http.StatusOK, "", nil) if err != nil { suite.FailNow(err.Error()) } @@ -267,7 +269,7 @@ func (suite *FiltersTestSuite) TestPostFilterMinimal() { func (suite *FiltersTestSuite) TestPostFilterEmptyTitle() { title := "" context := []string{"home"} - _, err := suite.postFilter(&title, &context, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") + _, err := suite.postFilter(&title, &context, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "", nil) if err != nil { suite.FailNow(err.Error()) } @@ -275,7 +277,7 @@ func (suite *FiltersTestSuite) TestPostFilterEmptyTitle() { func (suite *FiltersTestSuite) TestPostFilterMissingTitle() { context := []string{"home"} - _, err := suite.postFilter(nil, &context, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") + _, err := suite.postFilter(nil, &context, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "", nil) if err != nil { suite.FailNow(err.Error()) } @@ -284,7 +286,7 @@ func (suite *FiltersTestSuite) TestPostFilterMissingTitle() { func (suite *FiltersTestSuite) TestPostFilterEmptyContext() { title := "GNU/Linux" context := []string{} - _, err := suite.postFilter(&title, &context, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") + _, err := suite.postFilter(&title, &context, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "", nil) if err != nil { suite.FailNow(err.Error()) } @@ -292,7 +294,7 @@ func (suite *FiltersTestSuite) TestPostFilterEmptyContext() { func (suite *FiltersTestSuite) TestPostFilterMissingContext() { title := "GNU/Linux" - _, err := suite.postFilter(&title, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") + _, err := suite.postFilter(&title, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "", nil) if err != nil { suite.FailNow(err.Error()) } @@ -301,8 +303,37 @@ func (suite *FiltersTestSuite) TestPostFilterMissingContext() { // Creating another filter with the same title should fail. func (suite *FiltersTestSuite) TestPostFilterTitleConflict() { title := suite.testFilters["local_account_1_filter_1"].Title - _, err := suite.postFilter(&title, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "") + _, err := suite.postFilter(&title, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "", nil) if err != nil { suite.FailNow(err.Error()) } } + +// postFilterWithExpiration creates a filter with optional expiration. +func (suite *FiltersTestSuite) postFilterWithExpiration(title *string, expiresIn *int, expiresInStr *string, requestJson *string) *apimodel.FilterV2 { + context := []string{"home"} + filter, err := suite.postFilter(title, &context, nil, expiresIn, expiresInStr, nil, nil, requestJson, http.StatusOK, "", nil) + if err != nil { + suite.FailNow(err.Error()) + } + return filter +} + +// Regression test for https://github.com/superseriousbusiness/gotosocial/issues/3497 +func (suite *FiltersTestSuite) TestPostFilterWithEmptyStringExpiration() { + title := "Form Crimes" + expiresInStr := "" + filter := suite.postFilterWithExpiration(&title, nil, &expiresInStr, nil) + suite.Nil(filter.ExpiresAt) +} + +// Regression test related to https://github.com/superseriousbusiness/gotosocial/issues/3497 +func (suite *FiltersTestSuite) TestPostFilterWithNullExpirationJSON() { + requestJson := `{ + "title": "JSON Crimes", + "context": ["home"], + "expires_in": null + }` + filter := suite.postFilterWithExpiration(nil, nil, nil, &requestJson) + suite.Nil(filter.ExpiresAt) +} diff --git a/internal/api/client/filters/v2/filterput.go b/internal/api/client/filters/v2/filterput.go index cde03360d..9c1b56dd6 100644 --- a/internal/api/client/filters/v2/filterput.go +++ b/internal/api/client/filters/v2/filterput.go @@ -269,12 +269,11 @@ func validateNormalizeUpdateFilter(form *apimodel.FilterUpdateRequestV2) error { } } - // Normalize filter expiry if necessary. - if form.ExpiresInI != nil { - // If we parsed this as JSON, expires_in - // may be either a float64 or a string. + // If `expires_in` was provided + // as JSON, then normalize it. + if form.ExpiresInI.IsSpecified() { var err error - form.ExpiresIn, err = apiutil.ParseDuration( + form.ExpiresIn, err = apiutil.ParseNullableDuration( form.ExpiresInI, "expires_in", ) @@ -283,11 +282,6 @@ func validateNormalizeUpdateFilter(form *apimodel.FilterUpdateRequestV2) error { } } - // Interpret zero as indefinite duration. - if form.ExpiresIn != nil && *form.ExpiresIn == 0 { - form.ExpiresIn = nil - } - // Normalize and validate updates. for i, formKeyword := range form.Keywords { if formKeyword.Keyword != nil { diff --git a/internal/api/client/filters/v2/filterput_test.go b/internal/api/client/filters/v2/filterput_test.go index d82d84b20..afa858ba9 100644 --- a/internal/api/client/filters/v2/filterput_test.go +++ b/internal/api/client/filters/v2/filterput_test.go @@ -36,7 +36,7 @@ import ( "github.com/superseriousbusiness/gotosocial/testrig" ) -func (suite *FiltersTestSuite) putFilter(filterID string, title *string, context *[]string, action *string, expiresIn *int, keywordsAttributesID *[]string, keywordsAttributesKeyword *[]string, keywordsAttributesWholeWord *[]bool, keywordsAttributesDestroy *[]bool, statusesAttributesID *[]string, statusesAttributesStatusID *[]string, statusesAttributesDestroy *[]bool, requestJson *string, expectedHTTPStatus int, expectedBody string) (*apimodel.FilterV2, error) { +func (suite *FiltersTestSuite) putFilter(filterID string, title *string, context *[]string, action *string, expiresIn *int, expiresInStr *string, keywordsAttributesKeyword *[]string, keywordsAttributesWholeWord *[]bool, keywordsAttributesDestroy *[]bool, statusesAttributesID *[]string, statusesAttributesStatusID *[]string, statusesAttributesDestroy *[]bool, requestJson *string, expectedHTTPStatus int, expectedBody string, keywordsAttributesID *[]string) (*apimodel.FilterV2, error) { // instantiate recorder + test context recorder := httptest.NewRecorder() ctx, _ := testrig.CreateGinTestContext(recorder, nil) @@ -64,6 +64,8 @@ func (suite *FiltersTestSuite) putFilter(filterID string, title *string, context } if expiresIn != nil { ctx.Request.Form["expires_in"] = []string{strconv.Itoa(*expiresIn)} + } else if expiresInStr != nil { + ctx.Request.Form["expires_in"] = []string{*expiresInStr} } if keywordsAttributesID != nil { ctx.Request.Form["keywords_attributes[][id]"] = *keywordsAttributesID @@ -159,7 +161,7 @@ func (suite *FiltersTestSuite) TestPutFilterFull() { keywordsAttributesWholeWord := []bool{true, false, true} keywordsAttributesDestroy := []bool{false, true} statusesAttributesStatusID := []string{suite.testStatuses["remote_account_1_status_2"].ID} - filter, err := suite.putFilter(id, &title, &context, &action, &expiresIn, &keywordsAttributesID, &keywordsAttributesKeyword, &keywordsAttributesWholeWord, &keywordsAttributesDestroy, nil, &statusesAttributesStatusID, nil, nil, http.StatusOK, "") + filter, err := suite.putFilter(id, &title, &context, &action, &expiresIn, nil, &keywordsAttributesKeyword, &keywordsAttributesWholeWord, &keywordsAttributesDestroy, nil, &statusesAttributesStatusID, nil, nil, http.StatusOK, "", &keywordsAttributesID) if err != nil { suite.FailNow(err.Error()) } @@ -231,7 +233,7 @@ func (suite *FiltersTestSuite) TestPutFilterFullJSON() { } ] }` - filter, err := suite.putFilter(id, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "") + filter, err := suite.putFilter(id, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "", nil) if err != nil { suite.FailNow(err.Error()) } @@ -281,7 +283,7 @@ func (suite *FiltersTestSuite) TestPutFilterMinimal() { id := suite.testFilters["local_account_1_filter_1"].ID title := "GNU/Linux" context := []string{"home"} - filter, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusOK, "") + filter, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusOK, "", nil) if err != nil { suite.FailNow(err.Error()) } @@ -302,7 +304,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyTitle() { id := suite.testFilters["local_account_1_filter_1"].ID title := "" context := []string{"home"} - _, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: filter title must be provided, and must be no more than 200 chars"}`) + _, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: filter title must be provided, and must be no more than 200 chars"}`, nil) if err != nil { suite.FailNow(err.Error()) } @@ -312,7 +314,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyContext() { id := suite.testFilters["local_account_1_filter_1"].ID title := "GNU/Linux" context := []string{} - _, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: at least one filter context is required"}`) + _, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: at least one filter context is required"}`, nil) if err != nil { suite.FailNow(err.Error()) } @@ -322,7 +324,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyContext() { func (suite *FiltersTestSuite) TestPutFilterTitleConflict() { id := suite.testFilters["local_account_1_filter_1"].ID title := suite.testFilters["local_account_1_filter_2"].Title - _, err := suite.putFilter(id, &title, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusConflict, `{"error":"Conflict: you already have a filter with this title"}`) + _, err := suite.putFilter(id, &title, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusConflict, `{"error":"Conflict: you already have a filter with this title"}`, nil) if err != nil { suite.FailNow(err.Error()) } @@ -332,7 +334,7 @@ func (suite *FiltersTestSuite) TestPutAnotherAccountsFilter() { id := suite.testFilters["local_account_2_filter_1"].ID title := "GNU/Linux" context := []string{"home"} - _, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`) + _, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`, nil) if err != nil { suite.FailNow(err.Error()) } @@ -342,8 +344,70 @@ func (suite *FiltersTestSuite) TestPutNonexistentFilter() { id := "not_even_a_real_ULID" phrase := "GNU/Linux" context := []string{"home"} - _, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`) + _, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`, nil) if err != nil { suite.FailNow(err.Error()) } } + +// setFilterExpiration sets filter expiration. +func (suite *FiltersTestSuite) setFilterExpiration(id string, expiresIn *int, expiresInStr *string, requestJson *string) *apimodel.FilterV2 { + filter, err := suite.putFilter(id, nil, nil, nil, expiresIn, expiresInStr, nil, nil, nil, nil, nil, nil, requestJson, http.StatusOK, "", nil) + if err != nil { + suite.FailNow(err.Error()) + } + return filter +} + +// Regression test for https://github.com/superseriousbusiness/gotosocial/issues/3497 +func (suite *FiltersTestSuite) TestPutFilterUnsetExpirationDateEmptyString() { + id := suite.testFilters["local_account_1_filter_2"].ID + + // Setup: set an expiration date for the filter. + expiresIn := 86400 + filter := suite.setFilterExpiration(id, &expiresIn, nil, nil) + if !suite.NotNil(filter.ExpiresAt) { + suite.FailNow("Test precondition failed") + } + + // Unset the filter's expiration date by setting it to an empty string. + expiresInStr := "" + filter = suite.setFilterExpiration(id, nil, &expiresInStr, nil) + suite.Nil(filter.ExpiresAt) +} + +// Regression test related to https://github.com/superseriousbusiness/gotosocial/issues/3497 +func (suite *FiltersTestSuite) TestPutFilterUnsetExpirationDateNullJSON() { + id := suite.testFilters["local_account_1_filter_3"].ID + + // Setup: set an expiration date for the filter. + expiresIn := 86400 + filter := suite.setFilterExpiration(id, &expiresIn, nil, nil) + if !suite.NotNil(filter.ExpiresAt) { + suite.FailNow("Test precondition failed") + } + + // Unset the filter's expiration date by setting it to a null literal. + requestJson := `{ + "expires_in": null + }` + filter = suite.setFilterExpiration(id, nil, nil, &requestJson) + suite.Nil(filter.ExpiresAt) +} + +// Regression test related to https://github.com/superseriousbusiness/gotosocial/issues/3497 +func (suite *FiltersTestSuite) TestPutFilterUnalteredExpirationDateJSON() { + id := suite.testFilters["local_account_1_filter_4"].ID + + // Setup: set an expiration date for the filter. + expiresIn := 86400 + filter := suite.setFilterExpiration(id, &expiresIn, nil, nil) + if !suite.NotNil(filter.ExpiresAt) { + suite.FailNow("Test precondition failed") + } + + // Update nothing. There should still be an expiration date. + requestJson := `{}` + filter = suite.setFilterExpiration(id, nil, nil, &requestJson) + suite.NotNil(filter.ExpiresAt) +} diff --git a/internal/api/model/filterv1.go b/internal/api/model/filterv1.go index 1c3b5fb8e..0b092627e 100644 --- a/internal/api/model/filterv1.go +++ b/internal/api/model/filterv1.go @@ -95,5 +95,5 @@ type FilterCreateUpdateRequestV1 struct { // Number of seconds from now that the filter should expire. If omitted, filter never expires. // // Example: 86400 - ExpiresInI interface{} `json:"expires_in"` + ExpiresInI Nullable[any] `json:"expires_in"` } diff --git a/internal/api/model/filterv2.go b/internal/api/model/filterv2.go index 242c569dc..26b1b22b3 100644 --- a/internal/api/model/filterv2.go +++ b/internal/api/model/filterv2.go @@ -134,7 +134,7 @@ type FilterCreateRequestV2 struct { // Number of seconds from now that the filter should expire. If omitted, filter never expires. // // Example: 86400 - ExpiresInI interface{} `json:"expires_in"` + ExpiresInI Nullable[any] `json:"expires_in"` // Keywords to be added to the newly created filter. Keywords []FilterKeywordCreateUpdateRequest `form:"-" json:"keywords_attributes" xml:"keywords_attributes"` @@ -199,7 +199,7 @@ type FilterUpdateRequestV2 struct { // Number of seconds from now that the filter should expire. If omitted, filter never expires. // // Example: 86400 - ExpiresInI interface{} `json:"expires_in"` + ExpiresInI Nullable[any] `json:"expires_in"` // Keywords to be added to the filter, modified, or removed. Keywords []FilterKeywordCreateUpdateDeleteRequest `form:"-" json:"keywords_attributes" xml:"keywords_attributes"` diff --git a/internal/api/model/nullable.go b/internal/api/model/nullable.go new file mode 100644 index 000000000..4dd02f854 --- /dev/null +++ b/internal/api/model/nullable.go @@ -0,0 +1,107 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package model + +import ( + "bytes" + "encoding/json" + "errors" +) + +// Nullable is a generic type, which implements a field that can be one of three states: +// +// - field is not set in the request +// - field is explicitly set to `null` in the request +// - field is explicitly set to a valid value in the request +// +// Nullable is intended to be used with JSON unmarshalling. +// +// Adapted from https://github.com/oapi-codegen/nullable/blob/main/nullable.go +type Nullable[T any] struct { + state nullableState + value T +} + +type nullableState uint8 + +const ( + nullableStateUnspecified nullableState = 0 + nullableStateNull nullableState = 1 + nullableStateSet nullableState = 2 +) + +// Get retrieves the underlying value, if present, +// and returns an error if the value was not present. +func (t Nullable[T]) Get() (T, error) { + var empty T + if t.IsNull() { + return empty, errors.New("value is null") + } + + if !t.IsSpecified() { + return empty, errors.New("value is not specified") + } + + return t.value, nil +} + +// IsNull indicates whether the field +// was sent, and had a value of `null` +func (t Nullable[T]) IsNull() bool { + return t.state == nullableStateNull +} + +// IsSpecified indicates whether the field +// was sent either as a value or as `null`. +func (t Nullable[T]) IsSpecified() bool { + return t.state != nullableStateUnspecified +} + +// If field is unspecified, +// UnmarshalJSON won't be called. +func (t *Nullable[T]) UnmarshalJSON(data []byte) error { + // If field is specified as `null`. + if bytes.Equal(data, []byte("null")) { + t.setNull() + return nil + } + + // Otherwise, we have an + // actual value, so parse it. + var v T + if err := json.Unmarshal(data, &v); err != nil { + return err + } + + t.set(v) + return nil +} + +// setNull indicates that the field +// was sent, and had a value of `null` +func (t *Nullable[T]) setNull() { + *t = Nullable[T]{state: nullableStateNull} +} + +// set the underlying value to given value. +func (t *Nullable[T]) set(value T) { + *t = Nullable[T]{ + state: nullableStateSet, + value: value, + } +} diff --git a/internal/api/util/parseform.go b/internal/api/util/parseform.go index 19e24189f..3eab065f2 100644 --- a/internal/api/util/parseform.go +++ b/internal/api/util/parseform.go @@ -20,12 +20,13 @@ package util import ( "fmt" "strconv" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/util" ) -// ParseDuration parses the given raw interface belonging to +// ParseDuration parses the given raw interface belonging // the given fieldName as an integer duration. -// -// Will return nil, nil if rawI is the zero value of its type. func ParseDuration(rawI any, fieldName string) (*int, error) { var ( asInteger int @@ -60,11 +61,28 @@ func ParseDuration(rawI any, fieldName string) (*int, error) { return nil, err } - // Someone submitted 0, - // don't point to this. - if asInteger == 0 { - return nil, nil - } - return &asInteger, nil } + +// ParseNullableDuration is like ParseDuration, but +// for JSON values that may have been sent as `null`. +// +// IsSpecified should be checked and "true" on the +// given nullable before calling this function. +func ParseNullableDuration( + nullable apimodel.Nullable[any], + fieldName string, +) (*int, error) { + if nullable.IsNull() { + // Was specified as `null`, + // return pointer to zero value. + return util.Ptr(0), nil + } + + rawI, err := nullable.Get() + if err != nil { + return nil, err + } + + return ParseDuration(rawI, fieldName) +} diff --git a/internal/processing/filters/v1/create.go b/internal/processing/filters/v1/create.go index 18367dfce..86019d2a6 100644 --- a/internal/processing/filters/v1/create.go +++ b/internal/processing/filters/v1/create.go @@ -43,7 +43,7 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form if *form.Irreversible { filter.Action = gtsmodel.FilterActionHide } - if form.ExpiresIn != nil { + if form.ExpiresIn != nil && *form.ExpiresIn != 0 { filter.ExpiresAt = time.Now().Add(time.Second * time.Duration(*form.ExpiresIn)) } for _, context := range form.Context { diff --git a/internal/processing/filters/v1/update.go b/internal/processing/filters/v1/update.go index 81340b4be..15c5de365 100644 --- a/internal/processing/filters/v1/update.go +++ b/internal/processing/filters/v1/update.go @@ -67,7 +67,7 @@ func (p *Processor) Update( action = gtsmodel.FilterActionHide } expiresAt := time.Time{} - if form.ExpiresIn != nil { + if form.ExpiresIn != nil && *form.ExpiresIn != 0 { expiresAt = time.Now().Add(time.Second * time.Duration(*form.ExpiresIn)) } contextHome := false diff --git a/internal/processing/filters/v2/create.go b/internal/processing/filters/v2/create.go index 7095a643c..60dd46f43 100644 --- a/internal/processing/filters/v2/create.go +++ b/internal/processing/filters/v2/create.go @@ -41,7 +41,7 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form Title: form.Title, Action: typeutils.APIFilterActionToFilterAction(*form.FilterAction), } - if form.ExpiresIn != nil { + if form.ExpiresIn != nil && *form.ExpiresIn != 0 { filter.ExpiresAt = time.Now().Add(time.Second * time.Duration(*form.ExpiresIn)) } for _, context := range form.Context { diff --git a/internal/processing/filters/v2/update.go b/internal/processing/filters/v2/update.go index 0d443d58e..d5b5cce01 100644 --- a/internal/processing/filters/v2/update.go +++ b/internal/processing/filters/v2/update.go @@ -21,8 +21,6 @@ import ( "context" "errors" "fmt" - "time" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" @@ -30,6 +28,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/typeutils" "github.com/superseriousbusiness/gotosocial/internal/util" + "time" ) // Update an existing filter for the given account, using the provided parameters. @@ -68,10 +67,16 @@ func (p *Processor) Update( filterColumns = append(filterColumns, "action") filter.Action = typeutils.APIFilterActionToFilterAction(*form.FilterAction) } - // TODO: (Vyr) is it possible to unset a filter expiration with this API? if form.ExpiresIn != nil { + expiresIn := *form.ExpiresIn filterColumns = append(filterColumns, "expires_at") - filter.ExpiresAt = time.Now().Add(time.Second * time.Duration(*form.ExpiresIn)) + if expiresIn == 0 { + // Unset the expiration date. + filter.ExpiresAt = time.Time{} + } else { + // Update the expiration date. + filter.ExpiresAt = time.Now().Add(time.Second * time.Duration(expiresIn)) + } } if form.Context != nil { filterColumns = append(filterColumns, From 61f8f1e0e3236993f5522215f1900d35e49680c0 Mon Sep 17 00:00:00 2001 From: kim <89579420+NyaaaWhatsUpDoc@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:25:48 +0000 Subject: [PATCH 11/26] pull in ncruces/go-sqlite3 v0.20.3 with tetratelabs/wazero v1.8.2 (#3574) --- go.mod | 4 +- go.sum | 8 +- .../github.com/ncruces/go-sqlite3/README.md | 15 ++- .../ncruces/go-sqlite3/embed/README.md | 2 +- .../ncruces/go-sqlite3/embed/sqlite3.wasm | Bin 1401377 -> 1401582 bytes .../github.com/ncruces/go-sqlite3/go.work.sum | 1 + .../ncruces/go-sqlite3/vfs/README.md | 1 - .../github.com/ncruces/go-sqlite3/vfs/cksm.go | 8 ++ .../ncruces/go-sqlite3/vfs/const.go | 1 + .../github.com/ncruces/go-sqlite3/vfs/file.go | 2 +- .../ncruces/go-sqlite3/vfs/os_bsd.go | 25 +++-- .../ncruces/go-sqlite3/vfs/os_dotlk.go | 3 +- .../ncruces/go-sqlite3/vfs/os_windows.go | 9 +- .../ncruces/go-sqlite3/vfs/shm_bsd.go | 100 +++++------------- .../ncruces/go-sqlite3/vfs/shm_copy.go | 7 +- .../ncruces/go-sqlite3/vfs/shm_dotlk.go | 99 +++++------------ .../ncruces/go-sqlite3/vfs/shm_memlk.go | 55 ++++++++++ .../ncruces/go-sqlite3/vfs/shm_windows.go | 22 ++-- .../github.com/tetratelabs/wazero/README.md | 13 ++- .../tetratelabs/wazero/config_supported.go | 9 +- .../tetratelabs/wazero/config_unsupported.go | 2 +- .../tetratelabs/wazero/experimental/memory.go | 2 +- .../wazevo/backend/isa/amd64/machine.go | 2 +- .../backend/isa/arm64/machine_relocation.go | 15 ++- .../internal/engine/wazevo/backend/machine.go | 2 + .../wazero/internal/engine/wazevo/engine.go | 2 +- .../wazero/internal/platform/cpuid.go | 5 + .../wazero/internal/platform/cpuid_amd64.go | 9 +- .../wazero/internal/platform/cpuid_amd64.s | 4 +- .../wazero/internal/platform/cpuid_arm64.go | 71 +++++++++++++ .../wazero/internal/platform/cpuid_arm64.s | 21 ++++ .../internal/platform/cpuid_unsupported.go | 2 +- .../wazero/internal/platform/mmap_other.go | 2 +- .../wazero/internal/platform/mmap_unix.go | 17 +-- .../internal/platform/mmap_unsupported.go | 2 +- .../wazero/internal/platform/mprotect_bsd.go | 22 ++++ .../internal/platform/mprotect_syscall.go | 10 ++ .../internal/platform/mprotect_unsupported.go | 9 ++ .../wazero/internal/platform/platform.go | 9 +- .../internal/platform/platform_arm64.go | 4 +- vendor/modules.txt | 4 +- 41 files changed, 374 insertions(+), 226 deletions(-) create mode 100644 vendor/github.com/ncruces/go-sqlite3/vfs/shm_memlk.go create mode 100644 vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_arm64.go create mode 100644 vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_arm64.s create mode 100644 vendor/github.com/tetratelabs/wazero/internal/platform/mprotect_bsd.go create mode 100644 vendor/github.com/tetratelabs/wazero/internal/platform/mprotect_syscall.go create mode 100644 vendor/github.com/tetratelabs/wazero/internal/platform/mprotect_unsupported.go diff --git a/go.mod b/go.mod index d8e34f7d1..6f1ec2b26 100644 --- a/go.mod +++ b/go.mod @@ -62,7 +62,7 @@ require ( github.com/miekg/dns v1.1.62 github.com/minio/minio-go/v7 v7.0.80 github.com/mitchellh/mapstructure v1.5.0 - github.com/ncruces/go-sqlite3 v0.20.2 + github.com/ncruces/go-sqlite3 v0.20.3 github.com/oklog/ulid v1.3.1 github.com/prometheus/client_golang v1.20.5 github.com/spf13/cobra v1.8.1 @@ -73,7 +73,7 @@ require ( github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8 github.com/tdewolff/minify/v2 v2.21.2 github.com/technologize/otel-go-contrib v1.1.1 - github.com/tetratelabs/wazero v1.8.1 + github.com/tetratelabs/wazero v1.8.2 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 github.com/ulule/limiter/v3 v3.11.2 github.com/uptrace/bun v1.2.6 diff --git a/go.sum b/go.sum index ce53cf54b..d190203a1 100644 --- a/go.sum +++ b/go.sum @@ -434,8 +434,8 @@ github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/ncruces/go-sqlite3 v0.20.2 h1:cMLIwrLZQuCWVCEOowSqlIlpzgbag3jnYVW4NM5u01M= -github.com/ncruces/go-sqlite3 v0.20.2/go.mod h1:yL4ZNWGsr1/8pcLfpPW1RT1WFdvyeHonrgIwwi4rvkg= +github.com/ncruces/go-sqlite3 v0.20.3 h1:+4G4uEqOeusF0yRuQVUl9fuoEebUolwQSnBUjYBLYIw= +github.com/ncruces/go-sqlite3 v0.20.3/go.mod h1:ojLIAB243gtz68Eo283Ps+k9PyR3dvzS+9/RgId4+AA= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= @@ -553,8 +553,8 @@ github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03 github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/technologize/otel-go-contrib v1.1.1 h1:wZH9aSPNWZWIkEh3vfaKfMb15AJ80jJ1aVj/4GZdqIw= github.com/technologize/otel-go-contrib v1.1.1/go.mod h1:dCN/wj2WyUO8aFZFdIN+6tfJHImjTML/8r2YVYAy3So= -github.com/tetratelabs/wazero v1.8.1 h1:NrcgVbWfkWvVc4UtT4LRLDf91PsOzDzefMdwhLfA550= -github.com/tetratelabs/wazero v1.8.1/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= +github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4= +github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E= github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8= github.com/tidwall/buntdb v1.1.2 h1:noCrqQXL9EKMtcdwJcmuVKSEjqu1ua99RHHgbLTEHRo= diff --git a/vendor/github.com/ncruces/go-sqlite3/README.md b/vendor/github.com/ncruces/go-sqlite3/README.md index 935b9f254..f5394ab22 100644 --- a/vendor/github.com/ncruces/go-sqlite3/README.md +++ b/vendor/github.com/ncruces/go-sqlite3/README.md @@ -77,7 +77,7 @@ It also benefits greatly from [SQLite's](https://sqlite.org/testing.html) and Every commit is [tested](https://github.com/ncruces/go-sqlite3/wiki/Test-matrix) on Linux (amd64/arm64/386/riscv64/ppc64le/s390x), macOS (amd64/arm64), Windows (amd64), FreeBSD (amd64), OpenBSD (amd64), NetBSD (amd64), -illumos (amd64), and Solaris (amd64). +DragonFly BSD (amd64), illumos (amd64), and Solaris (amd64). The Go VFS is tested by running SQLite's [mptest](https://github.com/sqlite/sqlite/blob/master/mptest/mptest.c). @@ -90,9 +90,20 @@ Perfomance of the [`database/sql`](https://pkg.go.dev/database/sql) driver is The Wasm and VFS layers are also tested by running SQLite's [speedtest1](https://github.com/sqlite/sqlite/blob/master/test/speedtest1.c). +### FAQ, issues, new features + +For questions, please see [Discussions](https://github.com/ncruces/go-sqlite3/discussions/categories/q-a). + +Also, post there if you used this driver for something interesting +([_"Show and tell"_](https://github.com/ncruces/go-sqlite3/discussions/categories/show-and-tell)), +have an [idea](https://github.com/ncruces/go-sqlite3/discussions/categories/ideas)… + +The [Issue](https://github.com/ncruces/go-sqlite3/issues) tracker is for bugs we want fixed, +and features we're working on, planning to work on, or asking for help with. + ### Alternatives - [`modernc.org/sqlite`](https://pkg.go.dev/modernc.org/sqlite) - [`crawshaw.io/sqlite`](https://pkg.go.dev/crawshaw.io/sqlite) - [`github.com/mattn/go-sqlite3`](https://pkg.go.dev/github.com/mattn/go-sqlite3) -- [`github.com/zombiezen/go-sqlite`](https://pkg.go.dev/github.com/zombiezen/go-sqlite) \ No newline at end of file +- [`github.com/zombiezen/go-sqlite`](https://pkg.go.dev/github.com/zombiezen/go-sqlite) diff --git a/vendor/github.com/ncruces/go-sqlite3/embed/README.md b/vendor/github.com/ncruces/go-sqlite3/embed/README.md index b7b25c461..7a7a52a49 100644 --- a/vendor/github.com/ncruces/go-sqlite3/embed/README.md +++ b/vendor/github.com/ncruces/go-sqlite3/embed/README.md @@ -1,6 +1,6 @@ # Embeddable Wasm build of SQLite -This folder includes an embeddable Wasm build of SQLite 3.47.0 for use with +This folder includes an embeddable Wasm build of SQLite 3.47.1 for use with [`github.com/ncruces/go-sqlite3`](https://pkg.go.dev/github.com/ncruces/go-sqlite3). The following optional features are compiled in: diff --git a/vendor/github.com/ncruces/go-sqlite3/embed/sqlite3.wasm b/vendor/github.com/ncruces/go-sqlite3/embed/sqlite3.wasm index 173ad0e086d572c55752c13b2ca54cb100d6f863..2e86b6f5d233e1771cb68f15dc8deec9bc7c47c2 100644 GIT binary patch delta 4372 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 delta 4376 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 diff --git a/vendor/github.com/ncruces/go-sqlite3/go.work.sum b/vendor/github.com/ncruces/go-sqlite3/go.work.sum index 52265b555..c3936965c 100644 --- a/vendor/github.com/ncruces/go-sqlite3/go.work.sum +++ b/vendor/github.com/ncruces/go-sqlite3/go.work.sum @@ -9,5 +9,6 @@ golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/README.md b/vendor/github.com/ncruces/go-sqlite3/vfs/README.md index cf0e3c30f..08777972e 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/README.md +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/README.md @@ -30,7 +30,6 @@ like SQLite. You can also opt into a cross-platform locking implementation with the `sqlite3_dotlk` build tag. -The only requirement is an atomic `os.Mkdir`. Otherwise, file locking is not supported, and you must use [`nolock=1`](https://sqlite.org/uri.html#urinolock) diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/cksm.go b/vendor/github.com/ncruces/go-sqlite3/vfs/cksm.go index 900fa0952..42d7468f5 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/cksm.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/cksm.go @@ -101,6 +101,14 @@ func (c cksmFile) Pragma(name string, value string) (string, error) { return "", _NOTFOUND } +func (c cksmFile) DeviceCharacteristics() DeviceCharacteristic { + res := c.File.DeviceCharacteristics() + if c.verifyCksm { + res &^= IOCAP_SUBPAGE_READ + } + return res +} + func (c cksmFile) fileControl(ctx context.Context, mod api.Module, op _FcntlOpcode, pArg uint32) _ErrorCode { switch op { case _FCNTL_CKPT_START: diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/const.go b/vendor/github.com/ncruces/go-sqlite3/vfs/const.go index 0a8fee621..896cdaca4 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/const.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/const.go @@ -177,6 +177,7 @@ const ( IOCAP_POWERSAFE_OVERWRITE DeviceCharacteristic = 0x00001000 IOCAP_IMMUTABLE DeviceCharacteristic = 0x00002000 IOCAP_BATCH_ATOMIC DeviceCharacteristic = 0x00004000 + IOCAP_SUBPAGE_READ DeviceCharacteristic = 0x00008000 ) // https://sqlite.org/c3ref/c_fcntl_begin_atomic_write.html diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/file.go b/vendor/github.com/ncruces/go-sqlite3/vfs/file.go index ba70aa14f..b5d285375 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/file.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/file.go @@ -187,7 +187,7 @@ func (f *vfsFile) SectorSize() int { } func (f *vfsFile) DeviceCharacteristics() DeviceCharacteristic { - var res DeviceCharacteristic + res := IOCAP_SUBPAGE_READ if osBatchAtomic(f.File) { res |= IOCAP_BATCH_ATOMIC } diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/os_bsd.go b/vendor/github.com/ncruces/go-sqlite3/vfs/os_bsd.go index 56713e359..cc5da7cab 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/os_bsd.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/os_bsd.go @@ -15,9 +15,15 @@ func osGetSharedLock(file *os.File) _ErrorCode { func osGetReservedLock(file *os.File) _ErrorCode { rc := osLock(file, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK) if rc == _BUSY { - // The documentation states the lock is upgraded by releasing the previous lock, - // then acquiring the new lock. - // This is a race, so return BUSY_SNAPSHOT to ensure the transaction is aborted. + // The documentation states that a lock is upgraded by + // releasing the previous lock, then acquiring the new lock. + // Going over the source code of various BSDs, though, + // with LOCK_NB, the lock is not released, + // and EAGAIN is returned holding the shared lock. + // Still, if we're already in a transaction, we want to abort it, + // so return BUSY_SNAPSHOT here. If there's no transaction active, + // SQLite will change this back to SQLITE_BUSY, + // and invoke the busy handler if appropriate. return _BUSY_SNAPSHOT } return rc @@ -33,9 +39,11 @@ func osGetExclusiveLock(file *os.File, state *LockLevel) _ErrorCode { func osDowngradeLock(file *os.File, _ LockLevel) _ErrorCode { rc := osLock(file, unix.LOCK_SH|unix.LOCK_NB, _IOERR_RDLOCK) if rc == _BUSY { - // The documentation states the lock is upgraded by releasing the previous lock, - // then acquiring the new lock. - // This is a race, so return IOERR_RDLOCK to ensure the transaction is aborted. + // The documentation states that a lock is downgraded by + // releasing the previous lock then acquiring the new lock. + // Going over the source code of various BSDs, though, + // with LOCK_SH|LOCK_NB this should never happen. + // Return IOERR_RDLOCK, as BUSY would cause an assert to fail. return _IOERR_RDLOCK } return _OK @@ -50,7 +58,10 @@ func osReleaseLock(file *os.File, _ LockLevel) _ErrorCode { } func osCheckReservedLock(file *os.File) (bool, _ErrorCode) { - // Test the RESERVED lock. + // Test the RESERVED lock with fcntl(F_GETLK). + // This only works on systems where fcntl and flock are compatible. + // However, SQLite only calls this while holding a shared lock, + // so the difference is immaterial. lock, rc := osTestLock(file, _RESERVED_BYTE, 1) return lock == unix.F_WRLCK, rc } diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/os_dotlk.go b/vendor/github.com/ncruces/go-sqlite3/vfs/os_dotlk.go index 1c1a49c11..b00a1865b 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/os_dotlk.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/os_dotlk.go @@ -28,7 +28,8 @@ func osGetSharedLock(file *os.File) _ErrorCode { name := file.Name() locker := vfsDotLocks[name] if locker == nil { - err := os.Mkdir(name+".lock", 0777) + f, err := os.OpenFile(name+".lock", os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) + f.Close() if errors.Is(err, fs.ErrExist) { return _BUSY // Another process has the lock. } diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/os_windows.go b/vendor/github.com/ncruces/go-sqlite3/vfs/os_windows.go index b901f98aa..0b6e5d342 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/os_windows.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/os_windows.go @@ -50,14 +50,17 @@ func osGetExclusiveLock(file *os.File, state *LockLevel) _ErrorCode { if rc != _OK { // Reacquire the SHARED lock. - osReadLock(file, _SHARED_FIRST, _SHARED_SIZE, 0) + if rc := osReadLock(file, _SHARED_FIRST, _SHARED_SIZE, 0); rc != _OK { + // notest // this should never happen + return _IOERR_RDLOCK + } } return rc } func osDowngradeLock(file *os.File, state LockLevel) _ErrorCode { if state >= LOCK_EXCLUSIVE { - // Release the EXCLUSIVE lock. + // Release the EXCLUSIVE lock while holding the PENDING lock. osUnlock(file, _SHARED_FIRST, _SHARED_SIZE) // Reacquire the SHARED lock. @@ -78,7 +81,7 @@ func osDowngradeLock(file *os.File, state LockLevel) _ErrorCode { } func osReleaseLock(file *os.File, state LockLevel) _ErrorCode { - // Release all locks. + // Release all locks, PENDING must be last. if state >= LOCK_RESERVED { osUnlock(file, _RESERVED_BYTE, 1) } diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_bsd.go b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_bsd.go index d4e046369..07cabf7b5 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_bsd.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_bsd.go @@ -14,52 +14,52 @@ import ( "github.com/ncruces/go-sqlite3/internal/util" ) -type vfsShmFile struct { +type vfsShmParent struct { *os.File info os.FileInfo - refs int // +checklocks:vfsShmFilesMtx + refs int // +checklocks:vfsShmListMtx lock [_SHM_NLOCK]int16 // +checklocks:Mutex sync.Mutex } var ( - // +checklocks:vfsShmFilesMtx - vfsShmFiles []*vfsShmFile - vfsShmFilesMtx sync.Mutex + // +checklocks:vfsShmListMtx + vfsShmList []*vfsShmParent + vfsShmListMtx sync.Mutex ) type vfsShm struct { - *vfsShmFile + *vfsShmParent path string lock [_SHM_NLOCK]bool regions []*util.MappedRegion } func (s *vfsShm) Close() error { - if s.vfsShmFile == nil { + if s.vfsShmParent == nil { return nil } - vfsShmFilesMtx.Lock() - defer vfsShmFilesMtx.Unlock() + vfsShmListMtx.Lock() + defer vfsShmListMtx.Unlock() // Unlock everything. s.shmLock(0, _SHM_NLOCK, _SHM_UNLOCK) // Decrease reference count. - if s.vfsShmFile.refs > 0 { - s.vfsShmFile.refs-- - s.vfsShmFile = nil + if s.vfsShmParent.refs > 0 { + s.vfsShmParent.refs-- + s.vfsShmParent = nil return nil } err := s.File.Close() - for i, g := range vfsShmFiles { - if g == s.vfsShmFile { - vfsShmFiles[i] = nil - s.vfsShmFile = nil + for i, g := range vfsShmList { + if g == s.vfsShmParent { + vfsShmList[i] = nil + s.vfsShmParent = nil return err } } @@ -67,7 +67,7 @@ func (s *vfsShm) Close() error { } func (s *vfsShm) shmOpen() _ErrorCode { - if s.vfsShmFile != nil { + if s.vfsShmParent != nil { return _OK } @@ -85,13 +85,13 @@ func (s *vfsShm) shmOpen() _ErrorCode { return _IOERR_FSTAT } - vfsShmFilesMtx.Lock() - defer vfsShmFilesMtx.Unlock() + vfsShmListMtx.Lock() + defer vfsShmListMtx.Unlock() // Find a shared file, increase the reference count. - for _, g := range vfsShmFiles { + for _, g := range vfsShmList { if g != nil && os.SameFile(fi, g.info) { - s.vfsShmFile = g + s.vfsShmParent = g g.refs++ return _OK } @@ -107,18 +107,18 @@ func (s *vfsShm) shmOpen() _ErrorCode { } // Add the new shared file. - s.vfsShmFile = &vfsShmFile{ + s.vfsShmParent = &vfsShmParent{ File: f, info: fi, } f = nil // Don't close the file. - for i, g := range vfsShmFiles { + for i, g := range vfsShmList { if g == nil { - vfsShmFiles[i] = s.vfsShmFile + vfsShmList[i] = s.vfsShmParent return _OK } } - vfsShmFiles = append(vfsShmFiles, s.vfsShmFile) + vfsShmList = append(vfsShmList, s.vfsShmParent) return _OK } @@ -157,57 +157,11 @@ func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, ext func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) _ErrorCode { s.Lock() defer s.Unlock() - - switch { - case flags&_SHM_UNLOCK != 0: - for i := offset; i < offset+n; i++ { - if s.lock[i] { - if s.vfsShmFile.lock[i] == 0 { - panic(util.AssertErr()) - } - if s.vfsShmFile.lock[i] <= 0 { - s.vfsShmFile.lock[i] = 0 - } else { - s.vfsShmFile.lock[i]-- - } - s.lock[i] = false - } - } - case flags&_SHM_SHARED != 0: - for i := offset; i < offset+n; i++ { - if s.lock[i] { - panic(util.AssertErr()) - } - if s.vfsShmFile.lock[i]+1 <= 0 { - return _BUSY - } - } - for i := offset; i < offset+n; i++ { - s.vfsShmFile.lock[i]++ - s.lock[i] = true - } - case flags&_SHM_EXCLUSIVE != 0: - for i := offset; i < offset+n; i++ { - if s.lock[i] { - panic(util.AssertErr()) - } - if s.vfsShmFile.lock[i] != 0 { - return _BUSY - } - } - for i := offset; i < offset+n; i++ { - s.vfsShmFile.lock[i] = -1 - s.lock[i] = true - } - default: - panic(util.AssertErr()) - } - - return _OK + return s.shmMemLock(offset, n, flags) } func (s *vfsShm) shmUnmap(delete bool) { - if s.vfsShmFile == nil { + if s.vfsShmParent == nil { return } diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_copy.go b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_copy.go index 7a250523e..e6007aa1c 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_copy.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_copy.go @@ -31,7 +31,10 @@ const ( // // https://sqlite.org/walformat.html#the_wal_index_file_format -func (s *vfsShm) shmAcquire() { +func (s *vfsShm) shmAcquire(ptr *_ErrorCode) { + if ptr != nil && *ptr != _OK { + return + } if len(s.ptrs) == 0 || shmUnmodified(s.shadow[0][:], s.shared[0][:]) { return } @@ -69,7 +72,7 @@ func (s *vfsShm) shmRelease() { func (s *vfsShm) shmBarrier() { s.Lock() - s.shmAcquire() + s.shmAcquire(nil) s.shmRelease() s.Unlock() } diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_dotlk.go b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_dotlk.go index 36e00a1cd..4c7f47dec 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_dotlk.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_dotlk.go @@ -13,22 +13,22 @@ import ( "github.com/tetratelabs/wazero/api" ) -type vfsShmBuffer struct { +type vfsShmParent struct { shared [][_WALINDEX_PGSZ]byte - refs int // +checklocks:vfsShmBuffersMtx + refs int // +checklocks:vfsShmListMtx lock [_SHM_NLOCK]int16 // +checklocks:Mutex sync.Mutex } var ( - // +checklocks:vfsShmBuffersMtx - vfsShmBuffers = map[string]*vfsShmBuffer{} - vfsShmBuffersMtx sync.Mutex + // +checklocks:vfsShmListMtx + vfsShmList = map[string]*vfsShmParent{} + vfsShmListMtx sync.Mutex ) type vfsShm struct { - *vfsShmBuffer + *vfsShmParent mod api.Module alloc api.Function free api.Function @@ -40,20 +40,20 @@ type vfsShm struct { } func (s *vfsShm) Close() error { - if s.vfsShmBuffer == nil { + if s.vfsShmParent == nil { return nil } - vfsShmBuffersMtx.Lock() - defer vfsShmBuffersMtx.Unlock() + vfsShmListMtx.Lock() + defer vfsShmListMtx.Unlock() // Unlock everything. s.shmLock(0, _SHM_NLOCK, _SHM_UNLOCK) // Decrease reference count. - if s.vfsShmBuffer.refs > 0 { - s.vfsShmBuffer.refs-- - s.vfsShmBuffer = nil + if s.vfsShmParent.refs > 0 { + s.vfsShmParent.refs-- + s.vfsShmParent = nil return nil } @@ -61,22 +61,22 @@ func (s *vfsShm) Close() error { if err != nil && !errors.Is(err, fs.ErrNotExist) { return _IOERR_UNLOCK } - delete(vfsShmBuffers, s.path) - s.vfsShmBuffer = nil + delete(vfsShmList, s.path) + s.vfsShmParent = nil return nil } func (s *vfsShm) shmOpen() _ErrorCode { - if s.vfsShmBuffer != nil { + if s.vfsShmParent != nil { return _OK } - vfsShmBuffersMtx.Lock() - defer vfsShmBuffersMtx.Unlock() + vfsShmListMtx.Lock() + defer vfsShmListMtx.Unlock() // Find a shared buffer, increase the reference count. - if g, ok := vfsShmBuffers[s.path]; ok { - s.vfsShmBuffer = g + if g, ok := vfsShmList[s.path]; ok { + s.vfsShmParent = g g.refs++ return _OK } @@ -92,8 +92,8 @@ func (s *vfsShm) shmOpen() _ErrorCode { } // Add the new shared buffer. - s.vfsShmBuffer = &vfsShmBuffer{} - vfsShmBuffers[s.path] = s.vfsShmBuffer + s.vfsShmParent = &vfsShmParent{} + vfsShmList[s.path] = s.vfsShmParent return _OK } @@ -112,7 +112,7 @@ func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, ext s.Lock() defer s.Unlock() - defer s.shmAcquire() + defer s.shmAcquire(nil) // Extend shared memory. if int(id) >= len(s.shared) { @@ -125,7 +125,6 @@ func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, ext // Allocate shadow memory. if int(id) >= len(s.shadow) { s.shadow = append(s.shadow, make([][_WALINDEX_PGSZ]byte, int(id)-len(s.shadow)+1)...) - s.shadow[0][4] = 1 // force invalidation } // Allocate local memory. @@ -141,70 +140,26 @@ func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, ext s.ptrs = append(s.ptrs, uint32(s.stack[0])) } + s.shadow[0][4] = 1 return s.ptrs[id], _OK } -func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) _ErrorCode { +func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) (rc _ErrorCode) { s.Lock() defer s.Unlock() switch { case flags&_SHM_LOCK != 0: - defer s.shmAcquire() + defer s.shmAcquire(&rc) case flags&_SHM_EXCLUSIVE != 0: s.shmRelease() } - switch { - case flags&_SHM_UNLOCK != 0: - for i := offset; i < offset+n; i++ { - if s.lock[i] { - if s.vfsShmBuffer.lock[i] == 0 { - panic(util.AssertErr()) - } - if s.vfsShmBuffer.lock[i] <= 0 { - s.vfsShmBuffer.lock[i] = 0 - } else { - s.vfsShmBuffer.lock[i]-- - } - s.lock[i] = false - } - } - case flags&_SHM_SHARED != 0: - for i := offset; i < offset+n; i++ { - if s.lock[i] { - panic(util.AssertErr()) - } - if s.vfsShmBuffer.lock[i]+1 <= 0 { - return _BUSY - } - } - for i := offset; i < offset+n; i++ { - s.vfsShmBuffer.lock[i]++ - s.lock[i] = true - } - case flags&_SHM_EXCLUSIVE != 0: - for i := offset; i < offset+n; i++ { - if s.lock[i] { - panic(util.AssertErr()) - } - if s.vfsShmBuffer.lock[i] != 0 { - return _BUSY - } - } - for i := offset; i < offset+n; i++ { - s.vfsShmBuffer.lock[i] = -1 - s.lock[i] = true - } - default: - panic(util.AssertErr()) - } - - return _OK + return s.shmMemLock(offset, n, flags) } func (s *vfsShm) shmUnmap(delete bool) { - if s.vfsShmBuffer == nil { + if s.vfsShmParent == nil { return } defer s.Close() diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_memlk.go b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_memlk.go new file mode 100644 index 000000000..dc7b91350 --- /dev/null +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_memlk.go @@ -0,0 +1,55 @@ +//go:build ((freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !sqlite3_nosys) || sqlite3_flock || sqlite3_dotlk + +package vfs + +import "github.com/ncruces/go-sqlite3/internal/util" + +// +checklocks:s.Mutex +func (s *vfsShm) shmMemLock(offset, n int32, flags _ShmFlag) _ErrorCode { + switch { + case flags&_SHM_UNLOCK != 0: + for i := offset; i < offset+n; i++ { + if s.lock[i] { + if s.vfsShmParent.lock[i] == 0 { + panic(util.AssertErr()) + } + if s.vfsShmParent.lock[i] <= 0 { + s.vfsShmParent.lock[i] = 0 + } else { + s.vfsShmParent.lock[i]-- + } + s.lock[i] = false + } + } + case flags&_SHM_SHARED != 0: + for i := offset; i < offset+n; i++ { + if s.lock[i] { + panic(util.AssertErr()) + } + if s.vfsShmParent.lock[i]+1 <= 0 { + return _BUSY + } + } + for i := offset; i < offset+n; i++ { + s.vfsShmParent.lock[i]++ + s.lock[i] = true + } + case flags&_SHM_EXCLUSIVE != 0: + for i := offset; i < offset+n; i++ { + if s.lock[i] { + panic(util.AssertErr()) + } + if s.vfsShmParent.lock[i] != 0 { + return _BUSY + } + } + for i := offset; i < offset+n; i++ { + s.vfsShmParent.lock[i] = -1 + s.lock[i] = true + } + default: + panic(util.AssertErr()) + } + + return _OK +} diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_windows.go b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_windows.go index 218d8e2c7..374d491ac 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_windows.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_windows.go @@ -64,7 +64,7 @@ func (s *vfsShm) shmOpen() _ErrorCode { return osReadLock(s.File, _SHM_DMS, 1, time.Millisecond) } -func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, extend bool) (uint32, _ErrorCode) { +func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, extend bool) (_ uint32, rc _ErrorCode) { // Ensure size is a multiple of the OS page size. if size != _WALINDEX_PGSZ || (windows.Getpagesize()-1)&_WALINDEX_PGSZ != 0 { return 0, _IOERR_SHMMAP @@ -78,7 +78,7 @@ func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, ext return 0, rc } - defer s.shmAcquire() + defer s.shmAcquire(&rc) // Check if file is big enough. o, err := s.Seek(0, io.SeekEnd) @@ -107,7 +107,6 @@ func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, ext // Allocate shadow memory. if int(id) >= len(s.shadow) { s.shadow = append(s.shadow, make([][_WALINDEX_PGSZ]byte, int(id)-len(s.shadow)+1)...) - s.shadow[0][4] = 1 // force invalidation } // Allocate local memory. @@ -123,22 +122,23 @@ func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, ext s.ptrs = append(s.ptrs, uint32(s.stack[0])) } + s.shadow[0][4] = 1 return s.ptrs[id], _OK } -func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) _ErrorCode { - switch { - case flags&_SHM_LOCK != 0: - defer s.shmAcquire() - case flags&_SHM_EXCLUSIVE != 0: - s.shmRelease() - } - +func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) (rc _ErrorCode) { var timeout time.Duration if s.blocking { timeout = time.Millisecond } + switch { + case flags&_SHM_LOCK != 0: + defer s.shmAcquire(&rc) + case flags&_SHM_EXCLUSIVE != 0: + s.shmRelease() + } + switch { case flags&_SHM_UNLOCK != 0: return osUnlock(s.File, _SHM_BASE+uint32(offset), uint32(n)) diff --git a/vendor/github.com/tetratelabs/wazero/README.md b/vendor/github.com/tetratelabs/wazero/README.md index f020be99a..e49fcb8a8 100644 --- a/vendor/github.com/tetratelabs/wazero/README.md +++ b/vendor/github.com/tetratelabs/wazero/README.md @@ -96,14 +96,21 @@ systems are ones we test, but that doesn't necessarily mean other operating system versions won't work. We currently test Linux (Ubuntu and scratch), MacOS and Windows as packaged by -[GitHub Actions][11], as well compilation of 32-bit Linux and 64-bit FreeBSD. +[GitHub Actions][11], as well as nested VMs running on Linux for FreeBSD, NetBSD, +OpenBSD, DragonFly BSD, illumos and Solaris. + +We also test cross compilation for many `GOOS` and `GOARCH` combinations. * Interpreter * Linux is tested on amd64 (native) as well arm64 and riscv64 via emulation. - * MacOS and Windows are only tested on amd64. + * Windows, FreeBSD, NetBSD, OpenBSD, DragonFly BSD, illumos and Solaris are + tested only on amd64. + * macOS is tested only on arm64. * Compiler * Linux is tested on amd64 (native) as well arm64 via emulation. - * MacOS and Windows are only tested on amd64. + * Windows, FreeBSD, NetBSD, DragonFly BSD, illumos and Solaris are + tested only on amd64. + * macOS is tested only on arm64. wazero has no dependencies and doesn't require CGO. This means it can also be embedded in an application that doesn't use an operating system. This is a main diff --git a/vendor/github.com/tetratelabs/wazero/config_supported.go b/vendor/github.com/tetratelabs/wazero/config_supported.go index eb31ab935..214c2bb8c 100644 --- a/vendor/github.com/tetratelabs/wazero/config_supported.go +++ b/vendor/github.com/tetratelabs/wazero/config_supported.go @@ -5,10 +5,15 @@ // // Meanwhile, users who know their runtime.GOOS can operate with the compiler // may choose to use NewRuntimeConfigCompiler explicitly. -//go:build (amd64 || arm64) && (darwin || linux || freebsd || windows) +//go:build (amd64 || arm64) && (linux || darwin || freebsd || netbsd || dragonfly || solaris || windows) package wazero +import "github.com/tetratelabs/wazero/internal/platform" + func newRuntimeConfig() RuntimeConfig { - return NewRuntimeConfigCompiler() + if platform.CompilerSupported() { + return NewRuntimeConfigCompiler() + } + return NewRuntimeConfigInterpreter() } diff --git a/vendor/github.com/tetratelabs/wazero/config_unsupported.go b/vendor/github.com/tetratelabs/wazero/config_unsupported.go index 3e5a53cda..be56a4bc2 100644 --- a/vendor/github.com/tetratelabs/wazero/config_unsupported.go +++ b/vendor/github.com/tetratelabs/wazero/config_unsupported.go @@ -1,5 +1,5 @@ // This is the opposite constraint of config_supported.go -//go:build !(amd64 || arm64) || !(darwin || linux || freebsd || windows) +//go:build !(amd64 || arm64) || !(linux || darwin || freebsd || netbsd || dragonfly || solaris || windows) package wazero diff --git a/vendor/github.com/tetratelabs/wazero/experimental/memory.go b/vendor/github.com/tetratelabs/wazero/experimental/memory.go index 26540648b..8bf3aa35f 100644 --- a/vendor/github.com/tetratelabs/wazero/experimental/memory.go +++ b/vendor/github.com/tetratelabs/wazero/experimental/memory.go @@ -43,7 +43,7 @@ type LinearMemory interface { } // WithMemoryAllocator registers the given MemoryAllocator into the given -// context.Context. +// context.Context. The context must be passed when initializing a module. func WithMemoryAllocator(ctx context.Context, allocator MemoryAllocator) context.Context { if allocator != nil { return context.WithValue(ctx, expctxkeys.MemoryAllocatorKey{}, allocator) diff --git a/vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/machine.go b/vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/machine.go index aeeb6b645..7c27c92af 100644 --- a/vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/machine.go +++ b/vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/machine.go @@ -2196,7 +2196,7 @@ func (m *machine) Encode(ctx context.Context) (err error) { } // ResolveRelocations implements backend.Machine. -func (m *machine) ResolveRelocations(refToBinaryOffset []int, binary []byte, relocations []backend.RelocationInfo, _ []int) { +func (m *machine) ResolveRelocations(refToBinaryOffset []int, _ int, binary []byte, relocations []backend.RelocationInfo, _ []int) { for _, r := range relocations { offset := r.Offset calleeFnOffset := refToBinaryOffset[r.FuncRef] diff --git a/vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/machine_relocation.go b/vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/machine_relocation.go index 83902d927..932fe842b 100644 --- a/vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/machine_relocation.go +++ b/vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/machine_relocation.go @@ -21,7 +21,7 @@ const ( // trampolineIslandInterval is the range of the trampoline island. // Half of the range is used for the trampoline island, and the other half is used for the function. - trampolineIslandInterval = maxUnconditionalBranchOffset / 2 + trampolineIslandInterval = (maxUnconditionalBranchOffset - 1) / 2 // maxNumFunctions explicitly specifies the maximum number of functions that can be allowed in a single executable. maxNumFunctions = trampolineIslandInterval >> 6 @@ -42,12 +42,13 @@ func (m *machine) CallTrampolineIslandInfo(numFunctions int) (interval, size int // ResolveRelocations implements backend.Machine ResolveRelocations. func (m *machine) ResolveRelocations( refToBinaryOffset []int, + importedFns int, executable []byte, relocations []backend.RelocationInfo, callTrampolineIslandOffsets []int, ) { for _, islandOffset := range callTrampolineIslandOffsets { - encodeCallTrampolineIsland(refToBinaryOffset, islandOffset, executable) + encodeCallTrampolineIsland(refToBinaryOffset, importedFns, islandOffset, executable) } for _, r := range relocations { @@ -71,11 +72,15 @@ func (m *machine) ResolveRelocations( // encodeCallTrampolineIsland encodes a trampoline island for the given functions. // Each island consists of a trampoline instruction sequence for each function. // Each trampoline instruction sequence consists of 4 instructions + 32-bit immediate. -func encodeCallTrampolineIsland(refToBinaryOffset []int, islandOffset int, executable []byte) { - for i := 0; i < len(refToBinaryOffset); i++ { +func encodeCallTrampolineIsland(refToBinaryOffset []int, importedFns int, islandOffset int, executable []byte) { + // We skip the imported functions: they don't need trampolines + // and are not accounted for. + binaryOffsets := refToBinaryOffset[importedFns:] + + for i := 0; i < len(binaryOffsets); i++ { trampolineOffset := islandOffset + trampolineCallSize*i - fnOffset := refToBinaryOffset[i] + fnOffset := binaryOffsets[i] diff := fnOffset - (trampolineOffset + 16) if diff > math.MaxInt32 || diff < math.MinInt32 { // This case even amd64 can't handle. 4GB is too big. diff --git a/vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/machine.go b/vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/machine.go index 9044a9e4b..3a29e7cd6 100644 --- a/vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/machine.go +++ b/vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/machine.go @@ -77,11 +77,13 @@ type ( // ResolveRelocations resolves the relocations after emitting machine code. // * refToBinaryOffset: the map from the function reference (ssa.FuncRef) to the executable offset. + // * importedFns: the max index of the imported functions at the beginning of refToBinaryOffset // * executable: the binary to resolve the relocations. // * relocations: the relocations to resolve. // * callTrampolineIslandOffsets: the offsets of the trampoline islands in the executable. ResolveRelocations( refToBinaryOffset []int, + importedFns int, executable []byte, relocations []RelocationInfo, callTrampolineIslandOffsets []int, diff --git a/vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/engine.go b/vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/engine.go index f02b905fc..a6df3e7e7 100644 --- a/vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/engine.go +++ b/vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/engine.go @@ -314,7 +314,7 @@ func (e *engine) compileModule(ctx context.Context, module *wasm.Module, listene // Resolve relocations for local function calls. if len(rels) > 0 { - machine.ResolveRelocations(refToBinaryOffset, executable, rels, callTrampolineIslandOffsets) + machine.ResolveRelocations(refToBinaryOffset, importedFns, executable, rels, callTrampolineIslandOffsets) } if runtime.GOARCH == "arm64" { diff --git a/vendor/github.com/tetratelabs/wazero/internal/platform/cpuid.go b/vendor/github.com/tetratelabs/wazero/internal/platform/cpuid.go index 0dc6ec19c..0220d56fd 100644 --- a/vendor/github.com/tetratelabs/wazero/internal/platform/cpuid.go +++ b/vendor/github.com/tetratelabs/wazero/internal/platform/cpuid.go @@ -28,3 +28,8 @@ const ( CpuExtraFeatureAmd64ABM CpuFeature = 1 << 5 // Note: when adding new features, ensure that the feature is included in CpuFeatureFlags.Raw. ) + +const ( + // CpuFeatureArm64Atomic is the flag to query CpuFeatureFlags.Has for Large System Extensions capabilities on arm64 + CpuFeatureArm64Atomic CpuFeature = 1 << 21 +) diff --git a/vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_amd64.go b/vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_amd64.go index fbdb53936..a0c7734a0 100644 --- a/vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_amd64.go +++ b/vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_amd64.go @@ -1,4 +1,4 @@ -//go:build amd64 && !tinygo +//go:build gc package platform @@ -12,7 +12,7 @@ type cpuFeatureFlags struct { } // cpuid exposes the CPUID instruction to the Go layer (https://www.amd.com/system/files/TechDocs/25481.pdf) -// implemented in impl_amd64.s +// implemented in cpuid_amd64.s func cpuid(arg1, arg2 uint32) (eax, ebx, ecx, edx uint32) // cpuidAsBitmap combines the result of invoking cpuid to uint64 bitmap. @@ -60,8 +60,9 @@ func (f *cpuFeatureFlags) HasExtra(cpuFeature CpuFeature) bool { // Raw implements the same method on the CpuFeatureFlags interface. func (f *cpuFeatureFlags) Raw() uint64 { - // Below, we only set the first 4 bits for the features we care about, - // instead of setting all the unnecessary bits obtained from the CPUID instruction. + // Below, we only set bits for the features we care about, + // instead of setting all the unnecessary bits obtained from the + // CPUID instruction. var ret uint64 if f.Has(CpuFeatureAmd64SSE3) { ret = 1 << 0 diff --git a/vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_amd64.s b/vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_amd64.s index 8d483f3a6..4950ee629 100644 --- a/vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_amd64.s +++ b/vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_amd64.s @@ -1,6 +1,9 @@ +//go:build gc + #include "textflag.h" // lifted from github.com/intel-go/cpuid and src/internal/cpu/cpu_x86.s + // func cpuid(arg1, arg2 uint32) (eax, ebx, ecx, edx uint32) TEXT ·cpuid(SB), NOSPLIT, $0-24 MOVL arg1+0(FP), AX @@ -11,4 +14,3 @@ TEXT ·cpuid(SB), NOSPLIT, $0-24 MOVL CX, ecx+16(FP) MOVL DX, edx+20(FP) RET - diff --git a/vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_arm64.go b/vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_arm64.go new file mode 100644 index 000000000..5430353fd --- /dev/null +++ b/vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_arm64.go @@ -0,0 +1,71 @@ +//go:build gc + +package platform + +import "runtime" + +// CpuFeatures exposes the capabilities for this CPU, queried via the Has, HasExtra methods. +var CpuFeatures = loadCpuFeatureFlags() + +// cpuFeatureFlags implements CpuFeatureFlags interface. +type cpuFeatureFlags struct { + isar0 uint64 + isar1 uint64 +} + +// implemented in cpuid_arm64.s +func getisar0() uint64 + +// implemented in cpuid_arm64.s +func getisar1() uint64 + +func loadCpuFeatureFlags() CpuFeatureFlags { + switch runtime.GOOS { + case "darwin", "windows": + // These OSes do not allow userland to read the instruction set attribute registers, + // but basically require atomic instructions: + // - "darwin" is the desktop version (mobile version is "ios"), + // and the M1 is a ARMv8.4. + // - "windows" requires them from Windows 11, see page 12 + // https://download.microsoft.com/download/7/8/8/788bf5ab-0751-4928-a22c-dffdc23c27f2/Minimum%20Hardware%20Requirements%20for%20Windows%2011.pdf + return &cpuFeatureFlags{ + isar0: uint64(CpuFeatureArm64Atomic), + isar1: 0, + } + case "linux", "freebsd": + // These OSes allow userland to read the instruction set attribute registers, + // which is otherwise restricted to EL0: + // https://kernel.org/doc/Documentation/arm64/cpu-feature-registers.txt + // See these for contents of the registers: + // https://developer.arm.com/documentation/ddi0601/latest/AArch64-Registers/ID-AA64ISAR0-EL1--AArch64-Instruction-Set-Attribute-Register-0 + // https://developer.arm.com/documentation/ddi0601/latest/AArch64-Registers/ID-AA64ISAR1-EL1--AArch64-Instruction-Set-Attribute-Register-1 + return &cpuFeatureFlags{ + isar0: getisar0(), + isar1: getisar1(), + } + default: + return &cpuFeatureFlags{} + } +} + +// Has implements the same method on the CpuFeatureFlags interface. +func (f *cpuFeatureFlags) Has(cpuFeature CpuFeature) bool { + return (f.isar0 & uint64(cpuFeature)) != 0 +} + +// HasExtra implements the same method on the CpuFeatureFlags interface. +func (f *cpuFeatureFlags) HasExtra(cpuFeature CpuFeature) bool { + return (f.isar1 & uint64(cpuFeature)) != 0 +} + +// Raw implements the same method on the CpuFeatureFlags interface. +func (f *cpuFeatureFlags) Raw() uint64 { + // Below, we only set bits for the features we care about, + // instead of setting all the unnecessary bits obtained from the + // instruction set attribute registers. + var ret uint64 + if f.Has(CpuFeatureArm64Atomic) { + ret = 1 << 0 + } + return ret +} diff --git a/vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_arm64.s b/vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_arm64.s new file mode 100644 index 000000000..98305ad47 --- /dev/null +++ b/vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_arm64.s @@ -0,0 +1,21 @@ +//go:build gc + +#include "textflag.h" + +// lifted from github.com/golang/sys and cpu/cpu_arm64.s + +// func getisar0() uint64 +TEXT ·getisar0(SB), NOSPLIT, $0-8 + // get Instruction Set Attributes 0 into x0 + // mrs x0, ID_AA64ISAR0_EL1 = d5380600 + WORD $0xd5380600 + MOVD R0, ret+0(FP) + RET + +// func getisar1() uint64 +TEXT ·getisar1(SB), NOSPLIT, $0-8 + // get Instruction Set Attributes 1 into x0 + // mrs x0, ID_AA64ISAR1_EL1 = d5380620 + WORD $0xd5380620 + MOVD R0, ret+0(FP) + RET diff --git a/vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_unsupported.go b/vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_unsupported.go index 291bcea65..50a178f52 100644 --- a/vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_unsupported.go +++ b/vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_unsupported.go @@ -1,4 +1,4 @@ -//go:build !amd64 || tinygo +//go:build !(amd64 || arm64) || !gc package platform diff --git a/vendor/github.com/tetratelabs/wazero/internal/platform/mmap_other.go b/vendor/github.com/tetratelabs/wazero/internal/platform/mmap_other.go index ed5c40a4d..9f0610f27 100644 --- a/vendor/github.com/tetratelabs/wazero/internal/platform/mmap_other.go +++ b/vendor/github.com/tetratelabs/wazero/internal/platform/mmap_other.go @@ -1,5 +1,5 @@ // Separated from linux which has support for huge pages. -//go:build darwin || freebsd +//go:build darwin || freebsd || netbsd || dragonfly || solaris package platform diff --git a/vendor/github.com/tetratelabs/wazero/internal/platform/mmap_unix.go b/vendor/github.com/tetratelabs/wazero/internal/platform/mmap_unix.go index b0519003b..8d0baa712 100644 --- a/vendor/github.com/tetratelabs/wazero/internal/platform/mmap_unix.go +++ b/vendor/github.com/tetratelabs/wazero/internal/platform/mmap_unix.go @@ -1,10 +1,9 @@ -//go:build (darwin || linux || freebsd) && !tinygo +//go:build (linux || darwin || freebsd || netbsd || dragonfly || solaris) && !tinygo package platform import ( "syscall" - "unsafe" ) const ( @@ -31,17 +30,3 @@ func mmapCodeSegmentARM64(size int) ([]byte, error) { // The region must be RW: RW for writing native codes. return mmapCodeSegment(size, mmapProtARM64) } - -// MprotectRX is like syscall.Mprotect with RX permission, defined locally so that freebsd compiles. -func MprotectRX(b []byte) (err error) { - var _p0 unsafe.Pointer - if len(b) > 0 { - _p0 = unsafe.Pointer(&b[0]) - } - const prot = syscall.PROT_READ | syscall.PROT_EXEC - _, _, e1 := syscall.Syscall(syscall.SYS_MPROTECT, uintptr(_p0), uintptr(len(b)), uintptr(prot)) - if e1 != 0 { - err = syscall.Errno(e1) - } - return -} diff --git a/vendor/github.com/tetratelabs/wazero/internal/platform/mmap_unsupported.go b/vendor/github.com/tetratelabs/wazero/internal/platform/mmap_unsupported.go index 079aa643f..f3fa0911a 100644 --- a/vendor/github.com/tetratelabs/wazero/internal/platform/mmap_unsupported.go +++ b/vendor/github.com/tetratelabs/wazero/internal/platform/mmap_unsupported.go @@ -1,4 +1,4 @@ -//go:build !(darwin || linux || freebsd || windows) || tinygo +//go:build !(linux || darwin || freebsd || netbsd || dragonfly || solaris || windows) || tinygo package platform diff --git a/vendor/github.com/tetratelabs/wazero/internal/platform/mprotect_bsd.go b/vendor/github.com/tetratelabs/wazero/internal/platform/mprotect_bsd.go new file mode 100644 index 000000000..f8f40cabe --- /dev/null +++ b/vendor/github.com/tetratelabs/wazero/internal/platform/mprotect_bsd.go @@ -0,0 +1,22 @@ +//go:build (freebsd || netbsd || dragonfly) && !tinygo + +package platform + +import ( + "syscall" + "unsafe" +) + +// MprotectRX is like syscall.Mprotect with RX permission, defined locally so that BSD compiles. +func MprotectRX(b []byte) (err error) { + var _p0 unsafe.Pointer + if len(b) > 0 { + _p0 = unsafe.Pointer(&b[0]) + } + const prot = syscall.PROT_READ | syscall.PROT_EXEC + _, _, e1 := syscall.Syscall(syscall.SYS_MPROTECT, uintptr(_p0), uintptr(len(b)), uintptr(prot)) + if e1 != 0 { + err = syscall.Errno(e1) + } + return +} diff --git a/vendor/github.com/tetratelabs/wazero/internal/platform/mprotect_syscall.go b/vendor/github.com/tetratelabs/wazero/internal/platform/mprotect_syscall.go new file mode 100644 index 000000000..6fe96d6f6 --- /dev/null +++ b/vendor/github.com/tetratelabs/wazero/internal/platform/mprotect_syscall.go @@ -0,0 +1,10 @@ +//go:build (linux || darwin) && !tinygo + +package platform + +import "syscall" + +// MprotectRX is like syscall.Mprotect with RX permission. +func MprotectRX(b []byte) (err error) { + return syscall.Mprotect(b, syscall.PROT_READ|syscall.PROT_EXEC) +} diff --git a/vendor/github.com/tetratelabs/wazero/internal/platform/mprotect_unsupported.go b/vendor/github.com/tetratelabs/wazero/internal/platform/mprotect_unsupported.go new file mode 100644 index 000000000..84719ab08 --- /dev/null +++ b/vendor/github.com/tetratelabs/wazero/internal/platform/mprotect_unsupported.go @@ -0,0 +1,9 @@ +//go:build solaris && !tinygo + +package platform + +import "syscall" + +func MprotectRX(b []byte) error { + return syscall.ENOTSUP +} diff --git a/vendor/github.com/tetratelabs/wazero/internal/platform/platform.go b/vendor/github.com/tetratelabs/wazero/internal/platform/platform.go index a27556240..b9af094c1 100644 --- a/vendor/github.com/tetratelabs/wazero/internal/platform/platform.go +++ b/vendor/github.com/tetratelabs/wazero/internal/platform/platform.go @@ -11,15 +11,16 @@ import ( // archRequirementsVerified is set by platform-specific init to true if the platform is supported var archRequirementsVerified bool -// CompilerSupported is exported for tests and includes constraints here and also the assembler. +// CompilerSupported includes constraints here and also the assembler. func CompilerSupported() bool { switch runtime.GOOS { - case "darwin", "windows", "linux", "freebsd": + case "linux", "darwin", "freebsd", "netbsd", "dragonfly", "windows": + return archRequirementsVerified + case "solaris", "illumos": + return runtime.GOARCH == "amd64" && archRequirementsVerified default: return false } - - return archRequirementsVerified } // MmapCodeSegment copies the code into the executable region and returns the byte slice of the region. diff --git a/vendor/github.com/tetratelabs/wazero/internal/platform/platform_arm64.go b/vendor/github.com/tetratelabs/wazero/internal/platform/platform_arm64.go index caac58a3d..a8df707c7 100644 --- a/vendor/github.com/tetratelabs/wazero/internal/platform/platform_arm64.go +++ b/vendor/github.com/tetratelabs/wazero/internal/platform/platform_arm64.go @@ -2,6 +2,6 @@ package platform // init verifies that the current CPU supports the required ARM64 features func init() { - // No further checks currently needed. - archRequirementsVerified = true + // Ensure atomic instructions are supported. + archRequirementsVerified = CpuFeatures.Has(CpuFeatureArm64Atomic) } diff --git a/vendor/modules.txt b/vendor/modules.txt index 246e90c7d..07598062b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -520,7 +520,7 @@ github.com/modern-go/reflect2 # github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 ## explicit github.com/munnerz/goautoneg -# github.com/ncruces/go-sqlite3 v0.20.2 +# github.com/ncruces/go-sqlite3 v0.20.3 ## explicit; go 1.21 github.com/ncruces/go-sqlite3 github.com/ncruces/go-sqlite3/driver @@ -852,7 +852,7 @@ github.com/tdewolff/parse/v2/strconv # github.com/technologize/otel-go-contrib v1.1.1 ## explicit; go 1.17 github.com/technologize/otel-go-contrib/otelginmetrics -# github.com/tetratelabs/wazero v1.8.1 +# github.com/tetratelabs/wazero v1.8.2 ## explicit; go 1.21 github.com/tetratelabs/wazero github.com/tetratelabs/wazero/api From 65917f5bb98f1c0a0ce7285c284d25ea843c02c7 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:22:45 +0100 Subject: [PATCH 12/26] [bugfix] Log + ignore unknown notification types (#3577) * [bugfix] Log + ignore unknown notification types * pass context to ParseNotificationTypes --- .../client/notifications/notificationsget.go | 47 +++++++++++++------ .../notifications/notificationsget_test.go | 39 +++++++++++++++ internal/api/util/parsequery.go | 47 ------------------- internal/gtsmodel/notification.go | 36 +++++++++++++- 4 files changed, 106 insertions(+), 63 deletions(-) diff --git a/internal/api/client/notifications/notificationsget.go b/internal/api/client/notifications/notificationsget.go index 841768c63..7caadbe7d 100644 --- a/internal/api/client/notifications/notificationsget.go +++ b/internal/api/client/notifications/notificationsget.go @@ -18,11 +18,14 @@ package notifications import ( + "context" "net/http" "github.com/gin-gonic/gin" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/paging" ) @@ -151,18 +154,6 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) { return } - types, errWithCode := apiutil.ParseNotificationTypes(c.QueryArray(TypesKey)) - if errWithCode != nil { - apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) - return - } - - exclTypes, errWithCode := apiutil.ParseNotificationTypes(c.QueryArray(ExcludeTypesKey)) - if errWithCode != nil { - apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) - return - } - page, errWithCode := paging.ParseIDPage(c, 1, // min limit 80, // max limit @@ -173,12 +164,13 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) { return } + ctx := c.Request.Context() resp, errWithCode := m.processor.Timeline().NotificationsGet( - c.Request.Context(), + ctx, authed, page, - types, - exclTypes, + ParseNotificationTypes(ctx, c.QueryArray(TypesKey)), // Include types. + ParseNotificationTypes(ctx, c.QueryArray(ExcludeTypesKey)), // Exclude types. ) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) @@ -191,3 +183,28 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) { apiutil.JSON(c, http.StatusOK, resp.Items) } + +// ParseNotificationTypes converts the given slice of string values +// to gtsmodel notification types, logging + skipping unknown types. +func ParseNotificationTypes( + ctx context.Context, + values []string, +) []gtsmodel.NotificationType { + if len(values) == 0 { + return nil + } + + ntypes := make([]gtsmodel.NotificationType, 0, len(values)) + for _, value := range values { + ntype := gtsmodel.NewNotificationType(value) + if ntype == gtsmodel.NotificationUnknown { + // Type we don't know about (yet), log and ignore it. + log.Debugf(ctx, "ignoring unknown type %s", value) + continue + } + + ntypes = append(ntypes, ntype) + } + + return ntypes +} diff --git a/internal/api/client/notifications/notificationsget_test.go b/internal/api/client/notifications/notificationsget_test.go index 97d0e854b..5a6f83959 100644 --- a/internal/api/client/notifications/notificationsget_test.go +++ b/internal/api/client/notifications/notificationsget_test.go @@ -248,6 +248,45 @@ func (suite *NotificationsTestSuite) TestGetNotificationsIncludeOneType() { } } +// Test including an unknown notification type, it should be ignored. +func (suite *NotificationsTestSuite) TestGetNotificationsIncludeUnknownType() { + testAccount := suite.testAccounts["local_account_1"] + testToken := suite.testTokens["local_account_1"] + testUser := suite.testUsers["local_account_1"] + + suite.addMoreNotifications(testAccount) + + maxID := "" + minID := "" + limit := 10 + types := []string{"favourite", "something.weird"} + excludeTypes := []string(nil) + expectedHTTPStatus := http.StatusOK + expectedBody := "" + + notifications, _, err := suite.getNotifications( + testAccount, + testToken, + testUser, + maxID, + minID, + limit, + types, + excludeTypes, + expectedHTTPStatus, + expectedBody, + ) + if err != nil { + suite.FailNow(err.Error()) + } + + // This should only include the fav notification. + suite.Len(notifications, 1) + for _, notification := range notifications { + suite.Equal("favourite", notification.Type) + } +} + func TestBookmarkTestSuite(t *testing.T) { suite.Run(t, new(NotificationsTestSuite)) } diff --git a/internal/api/util/parsequery.go b/internal/api/util/parsequery.go index 9929524c5..9f4c02aed 100644 --- a/internal/api/util/parsequery.go +++ b/internal/api/util/parsequery.go @@ -18,13 +18,11 @@ package util import ( - "errors" "fmt" "strconv" "strings" "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) const ( @@ -218,51 +216,6 @@ func ParseInteractionReblogs(value string, defaultValue bool) (bool, gtserror.Wi return parseBool(value, defaultValue, InteractionReblogsKey) } -func ParseNotificationType(value string) (gtsmodel.NotificationType, gtserror.WithCode) { - switch strings.ToLower(value) { - case "follow": - return gtsmodel.NotificationFollow, nil - case "follow_request": - return gtsmodel.NotificationFollowRequest, nil - case "mention": - return gtsmodel.NotificationMention, nil - case "reblog": - return gtsmodel.NotificationReblog, nil - case "favourite": - return gtsmodel.NotificationFave, nil - case "poll": - return gtsmodel.NotificationPoll, nil - case "status": - return gtsmodel.NotificationStatus, nil - case "admin.sign_up": - return gtsmodel.NotificationSignup, nil - case "pending.favourite": - return gtsmodel.NotificationPendingFave, nil - case "pending.reply": - return gtsmodel.NotificationPendingReply, nil - case "pending.reblog": - return gtsmodel.NotificationPendingReblog, nil - default: - text := fmt.Sprintf("unrecognized notification type %s", value) - return 0, gtserror.NewErrorBadRequest(errors.New(text), text) - } -} - -func ParseNotificationTypes(values []string) ([]gtsmodel.NotificationType, gtserror.WithCode) { - if len(values) == 0 { - return nil, nil - } - ntypes := make([]gtsmodel.NotificationType, len(values)) - for i, value := range values { - ntype, errWithCode := ParseNotificationType(value) - if errWithCode != nil { - return nil, errWithCode - } - ntypes[i] = ntype - } - return ntypes, nil -} - /* Parse functions for *REQUIRED* parameters. */ diff --git a/internal/gtsmodel/notification.go b/internal/gtsmodel/notification.go index 49f1fe2bb..47bf7daa5 100644 --- a/internal/gtsmodel/notification.go +++ b/internal/gtsmodel/notification.go @@ -17,7 +17,10 @@ package gtsmodel -import "time" +import ( + "strings" + "time" +) // Notification models an alert/notification sent to an account about something like a reblog, like, new follow request, etc. type Notification struct { @@ -40,6 +43,7 @@ type NotificationType enumType const ( // Notification Types + NotificationUnknown NotificationType = 0 // NotificationUnknown -- unknown notification type, error if this occurs NotificationFollow NotificationType = 1 // NotificationFollow -- someone followed you NotificationFollowRequest NotificationType = 2 // NotificationFollowRequest -- someone requested to follow you NotificationMention NotificationType = 3 // NotificationMention -- someone mentioned you in their status @@ -82,3 +86,33 @@ func (t NotificationType) String() string { panic("invalid notification type") } } + +// NewNotificationType returns a notification type from the given value. +func NewNotificationType(in string) NotificationType { + switch strings.ToLower(in) { + case "follow": + return NotificationFollow + case "follow_request": + return NotificationFollowRequest + case "mention": + return NotificationMention + case "reblog": + return NotificationReblog + case "favourite": + return NotificationFave + case "poll": + return NotificationPoll + case "status": + return NotificationStatus + case "admin.sign_up": + return NotificationSignup + case "pending.favourite": + return NotificationPendingFave + case "pending.reply": + return NotificationPendingReply + case "pending.reblog": + return NotificationPendingReblog + default: + return NotificationUnknown + } +} From 312cb8b9c7e13802613fef33124a4570427e75a7 Mon Sep 17 00:00:00 2001 From: kim <89579420+NyaaaWhatsUpDoc@users.noreply.github.com> Date: Thu, 28 Nov 2024 11:54:22 +0000 Subject: [PATCH 13/26] [chore] rename New___(string) int signature functions to Parse___(string) int (#3580) * rename New___(string) int {} signature functions to Parse___(string) int {} * remove test output --- .../api/client/admin/domainpermissiondraftsget.go | 14 +++++--------- .../api/client/notifications/notificationsget.go | 12 ++++++------ .../20230828101322_admin_action_locking.go | 2 +- internal/gtsmodel/adminaction.go | 9 +++++---- internal/gtsmodel/domainpermission.go | 9 ++++++--- internal/gtsmodel/notification.go | 4 ++-- internal/processing/admin/accountaction.go | 2 +- 7 files changed, 26 insertions(+), 26 deletions(-) diff --git a/internal/api/client/admin/domainpermissiondraftsget.go b/internal/api/client/admin/domainpermissiondraftsget.go index dd3315857..d63179afc 100644 --- a/internal/api/client/admin/domainpermissiondraftsget.go +++ b/internal/api/client/admin/domainpermissiondraftsget.go @@ -147,16 +147,12 @@ func (m *Module) DomainPermissionDraftsGETHandler(c *gin.Context) { return } - permType := c.Query(apiutil.DomainPermissionPermTypeKey) - switch permType { - case "", "block", "allow": - // No problem. - - default: - // Invalid. + permTypeStr := c.Query(apiutil.DomainPermissionPermTypeKey) + permType := gtsmodel.ParseDomainPermissionType(permTypeStr) + if permType == gtsmodel.DomainPermissionUnknown { text := fmt.Sprintf( "permission_type %s not recognized, valid values are empty string, block, or allow", - permType, + permTypeStr, ) errWithCode := gtserror.NewErrorBadRequest(errors.New(text), text) apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) @@ -173,7 +169,7 @@ func (m *Module) DomainPermissionDraftsGETHandler(c *gin.Context) { c.Request.Context(), c.Query(apiutil.DomainPermissionSubscriptionIDKey), c.Query(apiutil.DomainPermissionDomainKey), - gtsmodel.NewDomainPermissionType(permType), + permType, page, ) if errWithCode != nil { diff --git a/internal/api/client/notifications/notificationsget.go b/internal/api/client/notifications/notificationsget.go index 7caadbe7d..b530c515d 100644 --- a/internal/api/client/notifications/notificationsget.go +++ b/internal/api/client/notifications/notificationsget.go @@ -169,8 +169,8 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) { ctx, authed, page, - ParseNotificationTypes(ctx, c.QueryArray(TypesKey)), // Include types. - ParseNotificationTypes(ctx, c.QueryArray(ExcludeTypesKey)), // Exclude types. + parseNotificationTypes(ctx, c.QueryArray(TypesKey)), // Include types. + parseNotificationTypes(ctx, c.QueryArray(ExcludeTypesKey)), // Exclude types. ) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) @@ -184,9 +184,9 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) { apiutil.JSON(c, http.StatusOK, resp.Items) } -// ParseNotificationTypes converts the given slice of string values +// parseNotificationTypes converts the given slice of string values // to gtsmodel notification types, logging + skipping unknown types. -func ParseNotificationTypes( +func parseNotificationTypes( ctx context.Context, values []string, ) []gtsmodel.NotificationType { @@ -196,10 +196,10 @@ func ParseNotificationTypes( ntypes := make([]gtsmodel.NotificationType, 0, len(values)) for _, value := range values { - ntype := gtsmodel.NewNotificationType(value) + ntype := gtsmodel.ParseNotificationType(value) if ntype == gtsmodel.NotificationUnknown { // Type we don't know about (yet), log and ignore it. - log.Debugf(ctx, "ignoring unknown type %s", value) + log.Warnf(ctx, "ignoring unknown type %s", value) continue } diff --git a/internal/db/bundb/migrations/20230828101322_admin_action_locking.go b/internal/db/bundb/migrations/20230828101322_admin_action_locking.go index b72976cc9..29c29a747 100644 --- a/internal/db/bundb/migrations/20230828101322_admin_action_locking.go +++ b/internal/db/bundb/migrations/20230828101322_admin_action_locking.go @@ -77,7 +77,7 @@ func init() { UpdatedAt: oldAction.UpdatedAt, TargetCategory: gtsmodel.AdminActionCategoryAccount, TargetID: oldAction.TargetAccountID, - Type: gtsmodel.NewAdminActionType(string(oldAction.Type)), + Type: gtsmodel.ParseAdminActionType(string(oldAction.Type)), AccountID: oldAction.AccountID, Text: oldAction.Text, SendEmail: util.Ptr(oldAction.SendEmail), diff --git a/internal/gtsmodel/adminaction.go b/internal/gtsmodel/adminaction.go index e8b82e495..5ca8244a0 100644 --- a/internal/gtsmodel/adminaction.go +++ b/internal/gtsmodel/adminaction.go @@ -19,6 +19,7 @@ package gtsmodel import ( "path" + "strings" "time" ) @@ -46,8 +47,8 @@ func (c AdminActionCategory) String() string { } } -func NewAdminActionCategory(in string) AdminActionCategory { - switch in { +func ParseAdminActionCategory(in string) AdminActionCategory { + switch strings.ToLower(in) { case "account": return AdminActionCategoryAccount case "domain": @@ -96,8 +97,8 @@ func (t AdminActionType) String() string { } } -func NewAdminActionType(in string) AdminActionType { - switch in { +func ParseAdminActionType(in string) AdminActionType { + switch strings.ToLower(in) { case "disable": return AdminActionDisable case "reenable": diff --git a/internal/gtsmodel/domainpermission.go b/internal/gtsmodel/domainpermission.go index 3d1ee873f..f1db4de59 100644 --- a/internal/gtsmodel/domainpermission.go +++ b/internal/gtsmodel/domainpermission.go @@ -17,7 +17,10 @@ package gtsmodel -import "time" +import ( + "strings" + "time" +) // DomainPermission models a domain permission // entry -- block / allow / draft / exclude. @@ -62,8 +65,8 @@ func (p DomainPermissionType) String() string { } } -func NewDomainPermissionType(in string) DomainPermissionType { - switch in { +func ParseDomainPermissionType(in string) DomainPermissionType { + switch strings.ToLower(in) { case "block": return DomainPermissionBlock case "allow": diff --git a/internal/gtsmodel/notification.go b/internal/gtsmodel/notification.go index 47bf7daa5..1ef805081 100644 --- a/internal/gtsmodel/notification.go +++ b/internal/gtsmodel/notification.go @@ -87,8 +87,8 @@ func (t NotificationType) String() string { } } -// NewNotificationType returns a notification type from the given value. -func NewNotificationType(in string) NotificationType { +// ParseNotificationType returns a notification type from the given value. +func ParseNotificationType(in string) NotificationType { switch strings.ToLower(in) { case "follow": return NotificationFollow diff --git a/internal/processing/admin/accountaction.go b/internal/processing/admin/accountaction.go index 7fd1047c4..59d4b420e 100644 --- a/internal/processing/admin/accountaction.go +++ b/internal/processing/admin/accountaction.go @@ -40,7 +40,7 @@ func (p *Processor) AccountAction( return "", gtserror.NewErrorInternalError(err) } - switch gtsmodel.NewAdminActionType(request.Type) { + switch gtsmodel.ParseAdminActionType(request.Type) { case gtsmodel.AdminActionSuspend: return p.accountActionSuspend(ctx, adminAcct, targetAcct, request.Text) From 3cc50491c23dca58ad01e5f07e9a0008b4fee937 Mon Sep 17 00:00:00 2001 From: kim <89579420+NyaaaWhatsUpDoc@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:14:49 +0000 Subject: [PATCH 14/26] pulls in the latest exif-terminator version with bugfix and performance optimizations (#3583) --- go.mod | 2 +- go.sum | 4 +- .../exif-terminator/jpeg.go | 16 ++-- .../exif-terminator/webp.go | 94 ++++++++++--------- vendor/modules.txt | 2 +- 5 files changed, 63 insertions(+), 55 deletions(-) diff --git a/go.mod b/go.mod index 6f1ec2b26..19fecf914 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( codeberg.org/gruf/go-sched v1.2.4 codeberg.org/gruf/go-storage v0.2.0 codeberg.org/gruf/go-structr v0.8.11 - codeberg.org/superseriousbusiness/exif-terminator v0.9.0 + codeberg.org/superseriousbusiness/exif-terminator v0.9.1 github.com/DmitriyVTitov/size v1.5.0 github.com/KimMachineGun/automemlimit v0.6.1 github.com/buckket/go-blurhash v1.1.0 diff --git a/go.sum b/go.sum index d190203a1..34fe33d84 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,8 @@ codeberg.org/gruf/go-storage v0.2.0 h1:mKj3Lx6AavEkuXXtxqPhdq+akW9YwrnP16yQBF7K5 codeberg.org/gruf/go-storage v0.2.0/go.mod h1:o3GzMDE5QNUaRnm/daUzFqvuAaC4utlgXDXYO79sWKU= codeberg.org/gruf/go-structr v0.8.11 h1:I3cQCHpK3fQSXWaaUfksAJRN4+efULiuF11Oi/m8c+o= codeberg.org/gruf/go-structr v0.8.11/go.mod h1:zkoXVrAnKosh8VFAsbP/Hhs8FmLBjbVVy5w/Ngm8ApM= -codeberg.org/superseriousbusiness/exif-terminator v0.9.0 h1:/EfyGI6HIrbkhFwgXGSjZ9o1kr/+k8v4mKdfXTH02Go= -codeberg.org/superseriousbusiness/exif-terminator v0.9.0/go.mod h1:gCWKduudUWFzsnixoMzu0FYVdxHWG+AbXnZ50DqxsUE= +codeberg.org/superseriousbusiness/exif-terminator v0.9.1 h1:8Pss29AVuvljHAYLnZUyoqJp/8IN1cD3Jz30bJbxme8= +codeberg.org/superseriousbusiness/exif-terminator v0.9.1/go.mod h1:gCWKduudUWFzsnixoMzu0FYVdxHWG+AbXnZ50DqxsUE= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= diff --git a/vendor/codeberg.org/superseriousbusiness/exif-terminator/jpeg.go b/vendor/codeberg.org/superseriousbusiness/exif-terminator/jpeg.go index 3c8b7035f..01ca313ca 100644 --- a/vendor/codeberg.org/superseriousbusiness/exif-terminator/jpeg.go +++ b/vendor/codeberg.org/superseriousbusiness/exif-terminator/jpeg.go @@ -109,17 +109,17 @@ func (v *jpegVisitor) writeSegment(s *jpegstructure.Segment) error { sizeLen, found := markerLen[s.MarkerId] if !found || sizeLen == 2 { - sizeLen = 2 - l := uint16(len(s.Data) + sizeLen) - - if err := binary.Write(w, binary.BigEndian, &l); err != nil { + l := uint16(len(s.Data) + 2) + b := make([]byte, 2) + binary.BigEndian.PutUint16(b, l) + if _, err := w.Write(b); err != nil { return err } - } else if sizeLen == 4 { - l := uint32(len(s.Data) + sizeLen) - - if err := binary.Write(w, binary.BigEndian, &l); err != nil { + l := uint32(len(s.Data) + 4) + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, l) + if _, err := w.Write(b); err != nil { return err } } else if sizeLen != 0 { diff --git a/vendor/codeberg.org/superseriousbusiness/exif-terminator/webp.go b/vendor/codeberg.org/superseriousbusiness/exif-terminator/webp.go index 392c4871d..b050f38fc 100644 --- a/vendor/codeberg.org/superseriousbusiness/exif-terminator/webp.go +++ b/vendor/codeberg.org/superseriousbusiness/exif-terminator/webp.go @@ -25,17 +25,16 @@ import ( ) const ( - riffHeaderSize = 4 * 3 + riffHeader = "RIFF" + webpHeader = "WEBP" + exifFourcc = "EXIF" + xmpFourcc = "XMP " ) var ( - riffHeader = [4]byte{'R', 'I', 'F', 'F'} - webpHeader = [4]byte{'W', 'E', 'B', 'P'} - exifFourcc = [4]byte{'E', 'X', 'I', 'F'} - xmpFourcc = [4]byte{'X', 'M', 'P', ' '} - errNoRiffHeader = errors.New("no RIFF header") errNoWebpHeader = errors.New("not a WEBP file") + errInvalidChunk = errors.New("invalid chunk") ) type webpVisitor struct { @@ -43,59 +42,68 @@ type webpVisitor struct { doneHeader bool } -func fourCC(b []byte) [4]byte { - return [4]byte{b[0], b[1], b[2], b[3]} -} - func (v *webpVisitor) split(data []byte, atEOF bool) (advance int, token []byte, err error) { // parse/write the header first if !v.doneHeader { - if len(data) < riffHeaderSize { - // need the full header + + // const rifHeaderSize = 12 + if len(data) < 12 { + if atEOF { + err = errNoRiffHeader + } return } - if fourCC(data) != riffHeader { + + if string(data[:4]) != riffHeader { err = errNoRiffHeader return } - if fourCC(data[8:]) != webpHeader { + + if string(data[8:12]) != webpHeader { err = errNoWebpHeader return } - if _, err = v.writer.Write(data[:riffHeaderSize]); err != nil { + + if _, err = v.writer.Write(data[:12]); err != nil { return } - advance += riffHeaderSize - data = data[riffHeaderSize:] + + advance += 12 + data = data[12:] v.doneHeader = true } - // need enough for fourcc and size - if len(data) < 8 { - return - } - size := int64(binary.LittleEndian.Uint32(data[4:])) - if (size & 1) != 0 { - // odd chunk size - extra padding byte - size++ - } - // wait until there is enough - if int64(len(data)-8) < size { - return - } - - fourcc := fourCC(data) - rawChunkData := data[8 : 8+size] - if fourcc == exifFourcc || fourcc == xmpFourcc { - // replace exif/xmp with blank - rawChunkData = make([]byte, size) - } - - if _, err = v.writer.Write(data[:8]); err == nil { - if _, err = v.writer.Write(rawChunkData); err == nil { - advance += 8 + int(size) + for { + // need enough for + // fourcc and size + if len(data) < 8 { + return } - } - return + size := int64(binary.LittleEndian.Uint32(data[4:])) + + if (size & 1) != 0 { + // odd chunk size: + // extra padding byte + size++ + } + + // wait until there is enough + if int64(len(data)) < 8+size { + return + } + + // replace exif/xmp with blank + switch string(data[:4]) { + case exifFourcc, xmpFourcc: + clear(data[8 : 8+size]) + } + + if _, err = v.writer.Write(data[:8+size]); err != nil { + return + } + + advance += 8 + int(size) + data = data[8+size:] + } } diff --git a/vendor/modules.txt b/vendor/modules.txt index 07598062b..4c57a75de 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -66,7 +66,7 @@ codeberg.org/gruf/go-storage/s3 # codeberg.org/gruf/go-structr v0.8.11 ## explicit; go 1.21 codeberg.org/gruf/go-structr -# codeberg.org/superseriousbusiness/exif-terminator v0.9.0 +# codeberg.org/superseriousbusiness/exif-terminator v0.9.1 ## explicit; go 1.21 codeberg.org/superseriousbusiness/exif-terminator # github.com/DmitriyVTitov/size v1.5.0 From d9f67efae512673c826b27daeae404a6051d9817 Mon Sep 17 00:00:00 2001 From: kim <89579420+NyaaaWhatsUpDoc@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:37:37 +0000 Subject: [PATCH 15/26] send out poll votes as separate create activities given that no other AP servers support multiple objects in a single activity (#3582) --- internal/processing/workers/federate.go | 17 ++++--- internal/typeutils/internaltoas.go | 42 +++++++++------- internal/typeutils/internaltoas_test.go | 64 +++++++++++++++---------- 3 files changed, 75 insertions(+), 48 deletions(-) diff --git a/internal/processing/workers/federate.go b/internal/processing/workers/federate.go index a0fd6bf69..8c08c42b7 100644 --- a/internal/processing/workers/federate.go +++ b/internal/processing/workers/federate.go @@ -217,18 +217,23 @@ func (f *federate) CreatePollVote(ctx context.Context, poll *gtsmodel.Poll, vote return err } - // Convert vote to AS Create with vote choices as Objects. - create, err := f.converter.PollVoteToASCreate(ctx, vote) + // Convert vote to AS Creates with vote choices as Objects. + creates, err := f.converter.PollVoteToASCreates(ctx, vote) if err != nil { return gtserror.Newf("error converting to notes: %w", err) } - // Send the Create via the Actor's outbox. - if _, err := f.FederatingActor().Send(ctx, outboxIRI, create); err != nil { - return gtserror.Newf("error sending Create activity via outbox %s: %w", outboxIRI, err) + var errs gtserror.MultiError + + // Send each create activity. + actor := f.FederatingActor() + for _, create := range creates { + if _, err := actor.Send(ctx, outboxIRI, create); err != nil { + errs.Appendf("error sending Create activity via outbox %s: %w", outboxIRI, err) + } } - return nil + return errs.Combine() } func (f *federate) DeleteStatus(ctx context.Context, status *gtsmodel.Status) error { diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index ed8bc1d8d..a81e5d2c0 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -1701,10 +1701,14 @@ func (c *Converter) ReportToASFlag(ctx context.Context, r *gtsmodel.Report) (voc // PollVoteToASCreate converts a vote on a poll into a Create // activity, suitable for federation, with each choice in the // vote appended as a Note to the Create's Object field. -func (c *Converter) PollVoteToASCreate( +// +// TODO: as soon as other AP server implementations support +// the use of multiple objects in a single create, update this +// to return just the one create event again. +func (c *Converter) PollVoteToASCreates( ctx context.Context, vote *gtsmodel.PollVote, -) (vocab.ActivityStreamsCreate, error) { +) ([]vocab.ActivityStreamsCreate, error) { if len(vote.Choices) == 0 { panic("no vote.Choices") } @@ -1743,22 +1747,25 @@ func (c *Converter) PollVoteToASCreate( return nil, gtserror.Newf("invalid account uri: %w", err) } - // Allocate Create activity and address 'To' poll author. - create := streams.NewActivityStreamsCreate() - ap.AppendTo(create, pollAuthorIRI) + // Parse each choice to a Note and add it to the list of Creates. + creates := make([]vocab.ActivityStreamsCreate, len(vote.Choices)) + for i, choice := range vote.Choices { - // Create ID formatted as: {$voterIRI}/activity#vote/{$statusIRI}. - id := author.URI + "/activity#vote/" + poll.Status.URI - ap.MustSet(ap.SetJSONLDIdStr, ap.WithJSONLDId(create), id) + // Allocate Create activity and address 'To' poll author. + create := streams.NewActivityStreamsCreate() + ap.AppendTo(create, pollAuthorIRI) - // Set Create actor appropriately. - ap.AppendActorIRIs(create, authorIRI) + // Create ID formatted as: {$voterIRI}/activity#vote{$index}/{$statusIRI}. + createID := fmt.Sprintf("%s/activity#vote%d/%s", author.URI, i, poll.Status.URI) + ap.MustSet(ap.SetJSONLDIdStr, ap.WithJSONLDId(create), createID) - // Set publish time for activity. - ap.SetPublished(create, vote.CreatedAt) + // Set Create actor appropriately. + ap.AppendActorIRIs(create, authorIRI) - // Parse each choice to a Note and add it to the Create. - for _, choice := range vote.Choices { + // Set publish time for activity. + ap.SetPublished(create, vote.CreatedAt) + + // Allocate new note to hold the vote. note := streams.NewActivityStreamsNote() // For AP IRI generate from author URI + poll ID + vote choice. @@ -1775,11 +1782,14 @@ func (c *Converter) PollVoteToASCreate( ap.AppendInReplyTo(note, statusIRI) ap.AppendTo(note, pollAuthorIRI) - // Append this note as Create Object. + // Append this note to the Create Object. appendStatusableToActivity(create, note, false) + + // Set create in slice. + creates[i] = create } - return create, nil + return creates, nil } // populateValuesForProp appends the given PolicyValues diff --git a/internal/typeutils/internaltoas_test.go b/internal/typeutils/internaltoas_test.go index a97eee2b8..c847cfc93 100644 --- a/internal/typeutils/internaltoas_test.go +++ b/internal/typeutils/internaltoas_test.go @@ -1104,43 +1104,55 @@ func (suite *InternalToASTestSuite) TestPinnedStatusesToASOneItem() { func (suite *InternalToASTestSuite) TestPollVoteToASCreate() { vote := suite.testPollVotes["remote_account_1_status_2_poll_vote_local_account_1"] - create, err := suite.typeconverter.PollVoteToASCreate(context.Background(), vote) - if err != nil { - suite.FailNow(err.Error()) - } + creates, err := suite.typeconverter.PollVoteToASCreates(context.Background(), vote) + suite.NoError(err) + suite.Len(creates, 2) - createI, err := ap.Serialize(create) + createI0, err := ap.Serialize(creates[0]) suite.NoError(err) - bytes, err := json.MarshalIndent(createI, "", " ") + createI1, err := ap.Serialize(creates[1]) + suite.NoError(err) + + bytes0, err := json.MarshalIndent(createI0, "", " ") + suite.NoError(err) + + bytes1, err := json.MarshalIndent(createI1, "", " ") suite.NoError(err) suite.Equal(`{ "@context": "https://www.w3.org/ns/activitystreams", "actor": "http://localhost:8080/users/the_mighty_zork", - "id": "http://localhost:8080/users/the_mighty_zork/activity#vote/http://fossbros-anonymous.io/users/foss_satan/statuses/01HEN2QRFA8H3C6QPN7RD4KSR6", - "object": [ - { - "attributedTo": "http://localhost:8080/users/the_mighty_zork", - "id": "http://localhost:8080/users/the_mighty_zork#01HEN2R65468ZG657C4ZPHJ4EX/votes/1", - "inReplyTo": "http://fossbros-anonymous.io/users/foss_satan/statuses/01HEN2QRFA8H3C6QPN7RD4KSR6", - "name": "tissues", - "to": "http://fossbros-anonymous.io/users/foss_satan", - "type": "Note" - }, - { - "attributedTo": "http://localhost:8080/users/the_mighty_zork", - "id": "http://localhost:8080/users/the_mighty_zork#01HEN2R65468ZG657C4ZPHJ4EX/votes/2", - "inReplyTo": "http://fossbros-anonymous.io/users/foss_satan/statuses/01HEN2QRFA8H3C6QPN7RD4KSR6", - "name": "financial times", - "to": "http://fossbros-anonymous.io/users/foss_satan", - "type": "Note" - } - ], + "id": "http://localhost:8080/users/the_mighty_zork/activity#vote0/http://fossbros-anonymous.io/users/foss_satan/statuses/01HEN2QRFA8H3C6QPN7RD4KSR6", + "object": { + "attributedTo": "http://localhost:8080/users/the_mighty_zork", + "id": "http://localhost:8080/users/the_mighty_zork#01HEN2R65468ZG657C4ZPHJ4EX/votes/1", + "inReplyTo": "http://fossbros-anonymous.io/users/foss_satan/statuses/01HEN2QRFA8H3C6QPN7RD4KSR6", + "name": "tissues", + "to": "http://fossbros-anonymous.io/users/foss_satan", + "type": "Note" + }, "published": "2021-09-11T11:45:37+02:00", "to": "http://fossbros-anonymous.io/users/foss_satan", "type": "Create" -}`, string(bytes)) +}`, string(bytes0)) + + suite.Equal(`{ + "@context": "https://www.w3.org/ns/activitystreams", + "actor": "http://localhost:8080/users/the_mighty_zork", + "id": "http://localhost:8080/users/the_mighty_zork/activity#vote1/http://fossbros-anonymous.io/users/foss_satan/statuses/01HEN2QRFA8H3C6QPN7RD4KSR6", + "object": { + "attributedTo": "http://localhost:8080/users/the_mighty_zork", + "id": "http://localhost:8080/users/the_mighty_zork#01HEN2R65468ZG657C4ZPHJ4EX/votes/2", + "inReplyTo": "http://fossbros-anonymous.io/users/foss_satan/statuses/01HEN2QRFA8H3C6QPN7RD4KSR6", + "name": "financial times", + "to": "http://fossbros-anonymous.io/users/foss_satan", + "type": "Note" + }, + "published": "2021-09-11T11:45:37+02:00", + "to": "http://fossbros-anonymous.io/users/foss_satan", + "type": "Create" +}`, string(bytes1)) } func (suite *InternalToASTestSuite) TestInteractionReqToASAcceptAnnounce() { From c9d36f7e452654bed04d50c6a468831a4136e6b3 Mon Sep 17 00:00:00 2001 From: kim <89579420+NyaaaWhatsUpDoc@users.noreply.github.com> Date: Fri, 29 Nov 2024 15:03:10 +0000 Subject: [PATCH 16/26] [performance] use new instance of bun.DB *after* migrations to reduce number of in-memory model schema (#3578) * use new instance of bun.DB *after* migrations to reduce number of model schema in-memory * update sqlite address comment --- internal/db/bundb/bundb.go | 122 +++++++++++++++++++------------------ 1 file changed, 63 insertions(+), 59 deletions(-) diff --git a/internal/db/bundb/bundb.go b/internal/db/bundb/bundb.go index 8756e086b..70132fe58 100644 --- a/internal/db/bundb/bundb.go +++ b/internal/db/bundb/bundb.go @@ -49,6 +49,7 @@ import ( "github.com/uptrace/bun/dialect/pgdialect" "github.com/uptrace/bun/dialect/sqlitedialect" "github.com/uptrace/bun/migrate" + "github.com/uptrace/bun/schema" ) // DBService satisfies the DB interface @@ -131,18 +132,18 @@ func doMigration(ctx context.Context, db *bun.DB) error { // NewBunDBService returns a bunDB derived from the provided config, which implements the go-fed DB interface. // Under the hood, it uses https://github.com/uptrace/bun to create and maintain a database connection. func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) { - var db *bun.DB + var sqldb *sql.DB + var dialect func() schema.Dialect var err error - t := strings.ToLower(config.GetDbType()) - switch t { + switch t := strings.ToLower(config.GetDbType()); t { case "postgres": - db, err = pgConn(ctx) + sqldb, dialect, err = pgConn(ctx) if err != nil { return nil, err } case "sqlite": - db, err = sqliteConn(ctx) + sqldb, dialect, err = sqliteConn(ctx) if err != nil { return nil, err } @@ -150,34 +151,20 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) { return nil, fmt.Errorf("database type %s not supported for bundb", t) } - // Add database query hooks. - db.AddQueryHook(queryHook{}) - if config.GetTracingEnabled() { - db.AddQueryHook(tracing.InstrumentBun()) - } - if config.GetMetricsEnabled() { - db.AddQueryHook(metrics.InstrumentBun()) - } - - // table registration is needed for many-to-many, see: - // https://bun.uptrace.dev/orm/many-to-many-relation/ - for _, t := range []interface{}{ - >smodel.AccountToEmoji{}, - >smodel.ConversationToStatus{}, - >smodel.StatusToEmoji{}, - >smodel.StatusToTag{}, - >smodel.ThreadToStatus{}, - } { - db.RegisterModel(t) - } - - // perform any pending database migrations: this includes - // the very first 'migration' on startup which just creates - // necessary tables - if err := doMigration(ctx, db); err != nil { + // perform any pending database migrations: this includes the first + // 'migration' on startup which just creates necessary db tables. + // + // Note this uses its own instance of bun.DB as bun will automatically + // store in-memory reflect type schema of any Go models passed to it, + // and we still maintain lots of old model versions in the migrations. + if err := doMigration(ctx, bunDB(sqldb, dialect)); err != nil { return nil, fmt.Errorf("db migration error: %s", err) } + // Wrap sql.DB as bun.DB type, + // adding any connection hooks. + db := bunDB(sqldb, dialect) + ps := &DBService{ Account: &accountDB{ db: db, @@ -319,17 +306,47 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) { return ps, nil } -func pgConn(ctx context.Context) (*bun.DB, error) { +// bunDB returns a new bun.DB for given sql.DB connection pool and dialect +// function. This can be used to apply any necessary opts / hooks as we +// initialize a bun.DB object both before and after performing migrations. +func bunDB(sqldb *sql.DB, dialect func() schema.Dialect) *bun.DB { + db := bun.NewDB(sqldb, dialect()) + + // Add our SQL connection hooks. + db.AddQueryHook(queryHook{}) + if config.GetTracingEnabled() { + db.AddQueryHook(tracing.InstrumentBun()) + } + if config.GetMetricsEnabled() { + db.AddQueryHook(metrics.InstrumentBun()) + } + + // table registration is needed for many-to-many, see: + // https://bun.uptrace.dev/orm/many-to-many-relation/ + for _, t := range []interface{}{ + >smodel.AccountToEmoji{}, + >smodel.ConversationToStatus{}, + >smodel.StatusToEmoji{}, + >smodel.StatusToTag{}, + >smodel.ThreadToStatus{}, + } { + db.RegisterModel(t) + } + + return db +} + +func pgConn(ctx context.Context) (*sql.DB, func() schema.Dialect, error) { opts, err := deriveBunDBPGOptions() //nolint:contextcheck if err != nil { - return nil, fmt.Errorf("could not create bundb postgres options: %w", err) + return nil, nil, fmt.Errorf("could not create bundb postgres options: %w", err) } cfg := stdlib.RegisterConnConfig(opts) sqldb, err := sql.Open("pgx-gts", cfg) if err != nil { - return nil, fmt.Errorf("could not open postgres db: %w", err) + return nil, nil, fmt.Errorf("could not open postgres db: %w", err) } // Tune db connections for postgres, see: @@ -339,22 +356,20 @@ func pgConn(ctx context.Context) (*bun.DB, error) { sqldb.SetMaxIdleConns(2) // assume default 2; if max idle is less than max open, it will be automatically adjusted sqldb.SetConnMaxLifetime(5 * time.Minute) // fine to kill old connections - db := bun.NewDB(sqldb, pgdialect.New()) - // ping to check the db is there and listening - if err := db.PingContext(ctx); err != nil { - return nil, fmt.Errorf("postgres ping: %w", err) + if err := sqldb.PingContext(ctx); err != nil { + return nil, nil, fmt.Errorf("postgres ping: %w", err) } log.Info(ctx, "connected to POSTGRES database") - return db, nil + return sqldb, func() schema.Dialect { return pgdialect.New() }, nil } -func sqliteConn(ctx context.Context) (*bun.DB, error) { +func sqliteConn(ctx context.Context) (*sql.DB, func() schema.Dialect, error) { // validate db address has actually been set address := config.GetDbAddress() if address == "" { - return nil, fmt.Errorf("'%s' was not set when attempting to start sqlite", config.DbAddressFlag()) + return nil, nil, fmt.Errorf("'%s' was not set when attempting to start sqlite", config.DbAddressFlag()) } // Build SQLite connection address with prefs. @@ -363,7 +378,7 @@ func sqliteConn(ctx context.Context) (*bun.DB, error) { // Open new DB instance sqldb, err := sql.Open("sqlite-gts", address) if err != nil { - return nil, fmt.Errorf("could not open sqlite db with address %s: %w", address, err) + return nil, nil, fmt.Errorf("could not open sqlite db with address %s: %w", address, err) } // Tune db connections for sqlite, see: @@ -379,16 +394,14 @@ func sqliteConn(ctx context.Context) (*bun.DB, error) { sqldb.SetConnMaxLifetime(5 * time.Minute) } - db := bun.NewDB(sqldb, sqlitedialect.New()) - // ping to check the db is there and listening - if err := db.PingContext(ctx); err != nil { - return nil, fmt.Errorf("sqlite ping: %w", err) + if err := sqldb.PingContext(ctx); err != nil { + return nil, nil, fmt.Errorf("sqlite ping: %w", err) } log.Infof(ctx, "connected to SQLITE database with address %s", address) - return db, nil + return sqldb, func() schema.Dialect { return sqlitedialect.New() }, nil } /* @@ -517,15 +530,12 @@ func buildSQLiteAddress(addr string) (string, bool) { // // - SQLite by itself supports setting a subset of its configuration options // via URI query arguments in the connection. Namely `mode` and `cache`. - // This is the same situation for the directly transpiled C->Go code in - // modernc.org/sqlite, i.e. modernc.org/sqlite/lib, NOT the Go SQL driver. + // This is the same situation for our supported SQLite implementations. // - // - `modernc.org/sqlite` has a "shim" around it to allow the directly - // transpiled C code to be usable with a more native Go API. This is in - // the form of a `database/sql/driver.Driver{}` implementation that calls - // through to the transpiled C code. + // - Both implementations have a "shim" around them in the form of a + // `database/sql/driver.Driver{}` implementation. // - // - The SQLite shim we interface with adds support for setting ANY of the + // - The SQLite shims we interface with add support for setting ANY of the // configuration options via query arguments, through using a special `_pragma` // query key that specifies SQLite PRAGMAs to set upon opening each connection. // As such you will see below that most config is set with the `_pragma` key. @@ -551,12 +561,6 @@ func buildSQLiteAddress(addr string) (string, bool) { // reached. And for whatever reason (:shrug:) SQLite is very particular about // setting this BEFORE the `journal_mode` is set, otherwise you can end up // running into more of these `SQLITE_BUSY` return codes than you might expect. - // - // - One final thing (I promise!): `SQLITE_BUSY` is only handled by the internal - // `busy_timeout` handler in the case that a data race occurs contending for - // table locks. THERE ARE STILL OTHER SITUATIONS IN WHICH THIS MAY BE RETURNED! - // As such, we use our wrapping DB{} and Tx{} types (in "db.go") which make use - // of our own retry-busy handler. // Drop anything fancy from DB address addr = strings.Split(addr, "?")[0] // drop any provided query strings From 936b269b056a3b65556f6de655a8610328a76e61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 10:44:43 +0100 Subject: [PATCH 17/26] [chore]: Bump github.com/minio/minio-go/v7 from 7.0.80 to 7.0.81 (#3590) Bumps [github.com/minio/minio-go/v7](https://github.com/minio/minio-go) from 7.0.80 to 7.0.81. - [Release notes](https://github.com/minio/minio-go/releases) - [Commits](https://github.com/minio/minio-go/compare/v7.0.80...v7.0.81) --- updated-dependencies: - dependency-name: github.com/minio/minio-go/v7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 +- .../minio/minio-go/v7/api-prompt-object.go | 78 + .../minio/minio-go/v7/api-prompt-options.go | 84 + .../minio-go/v7/api-put-object-fan-out.go | 5 +- vendor/github.com/minio/minio-go/v7/api.go | 2 +- .../minio/minio-go/v7/functional_tests.go | 1907 ++++------------- .../v7/pkg/credentials/sts_web_identity.go | 7 +- .../minio/minio-go/v7/post-policy.go | 71 +- .../minio/minio-go/v7/retry-continous.go | 10 +- vendor/github.com/minio/minio-go/v7/retry.go | 10 +- vendor/modules.txt | 2 +- 12 files changed, 603 insertions(+), 1579 deletions(-) create mode 100644 vendor/github.com/minio/minio-go/v7/api-prompt-object.go create mode 100644 vendor/github.com/minio/minio-go/v7/api-prompt-options.go diff --git a/go.mod b/go.mod index 19fecf914..ed2811b19 100644 --- a/go.mod +++ b/go.mod @@ -60,7 +60,7 @@ require ( github.com/k3a/html2text v1.2.1 github.com/microcosm-cc/bluemonday v1.0.27 github.com/miekg/dns v1.1.62 - github.com/minio/minio-go/v7 v7.0.80 + github.com/minio/minio-go/v7 v7.0.81 github.com/mitchellh/mapstructure v1.5.0 github.com/ncruces/go-sqlite3 v0.20.3 github.com/oklog/ulid v1.3.1 diff --git a/go.sum b/go.sum index 34fe33d84..4e33f2d61 100644 --- a/go.sum +++ b/go.sum @@ -413,8 +413,8 @@ github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.80 h1:2mdUHXEykRdY/BigLt3Iuu1otL0JTogT0Nmltg0wujk= -github.com/minio/minio-go/v7 v7.0.80/go.mod h1:84gmIilaX4zcvAWWzJ5Z1WI5axN+hAbM5w25xf8xvC0= +github.com/minio/minio-go/v7 v7.0.81 h1:SzhMN0TQ6T/xSBu6Nvw3M5M8voM+Ht8RH3hE8S7zxaA= +github.com/minio/minio-go/v7 v7.0.81/go.mod h1:84gmIilaX4zcvAWWzJ5Z1WI5axN+hAbM5w25xf8xvC0= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= diff --git a/vendor/github.com/minio/minio-go/v7/api-prompt-object.go b/vendor/github.com/minio/minio-go/v7/api-prompt-object.go new file mode 100644 index 000000000..dac062a75 --- /dev/null +++ b/vendor/github.com/minio/minio-go/v7/api-prompt-object.go @@ -0,0 +1,78 @@ +/* + * MinIO Go Library for Amazon S3 Compatible Cloud Storage + * Copyright 2015-2024 MinIO, Inc. + * + * 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. + */ + +package minio + +import ( + "bytes" + "context" + "io" + "net/http" + + "github.com/goccy/go-json" + "github.com/minio/minio-go/v7/pkg/s3utils" +) + +// PromptObject performs language model inference with the prompt and referenced object as context. +// Inference is performed using a Lambda handler that can process the prompt and object. +// Currently, this functionality is limited to certain MinIO servers. +func (c *Client) PromptObject(ctx context.Context, bucketName, objectName, prompt string, opts PromptObjectOptions) (io.ReadCloser, error) { + // Input validation. + if err := s3utils.CheckValidBucketName(bucketName); err != nil { + return nil, ErrorResponse{ + StatusCode: http.StatusBadRequest, + Code: "InvalidBucketName", + Message: err.Error(), + } + } + if err := s3utils.CheckValidObjectName(objectName); err != nil { + return nil, ErrorResponse{ + StatusCode: http.StatusBadRequest, + Code: "XMinioInvalidObjectName", + Message: err.Error(), + } + } + + opts.AddLambdaArnToReqParams(opts.LambdaArn) + opts.SetHeader("Content-Type", "application/json") + opts.AddPromptArg("prompt", prompt) + promptReqBytes, err := json.Marshal(opts.PromptArgs) + if err != nil { + return nil, err + } + + // Execute POST on bucket/object. + resp, err := c.executeMethod(ctx, http.MethodPost, requestMetadata{ + bucketName: bucketName, + objectName: objectName, + queryValues: opts.toQueryValues(), + customHeader: opts.Header(), + contentSHA256Hex: sum256Hex(promptReqBytes), + contentBody: bytes.NewReader(promptReqBytes), + contentLength: int64(len(promptReqBytes)), + }) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + defer closeResponse(resp) + return nil, httpRespToErrorResponse(resp, bucketName, objectName) + } + + return resp.Body, nil +} diff --git a/vendor/github.com/minio/minio-go/v7/api-prompt-options.go b/vendor/github.com/minio/minio-go/v7/api-prompt-options.go new file mode 100644 index 000000000..4493a75d4 --- /dev/null +++ b/vendor/github.com/minio/minio-go/v7/api-prompt-options.go @@ -0,0 +1,84 @@ +/* + * MinIO Go Library for Amazon S3 Compatible Cloud Storage + * Copyright 2015-2024 MinIO, Inc. + * + * 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. + */ + +package minio + +import ( + "net/http" + "net/url" +) + +// PromptObjectOptions provides options to PromptObject call. +// LambdaArn is the ARN of the Prompt Lambda to be invoked. +// PromptArgs is a map of key-value pairs to be passed to the inference action on the Prompt Lambda. +// "prompt" is a reserved key and should not be used as a key in PromptArgs. +type PromptObjectOptions struct { + LambdaArn string + PromptArgs map[string]any + headers map[string]string + reqParams url.Values +} + +// Header returns the http.Header representation of the POST options. +func (o PromptObjectOptions) Header() http.Header { + headers := make(http.Header, len(o.headers)) + for k, v := range o.headers { + headers.Set(k, v) + } + return headers +} + +// AddPromptArg Add a key value pair to the prompt arguments where the key is a string and +// the value is a JSON serializable. +func (o *PromptObjectOptions) AddPromptArg(key string, value any) { + if o.PromptArgs == nil { + o.PromptArgs = make(map[string]any) + } + o.PromptArgs[key] = value +} + +// AddLambdaArnToReqParams adds the lambdaArn to the request query string parameters. +func (o *PromptObjectOptions) AddLambdaArnToReqParams(lambdaArn string) { + if o.reqParams == nil { + o.reqParams = make(url.Values) + } + o.reqParams.Add("lambdaArn", lambdaArn) +} + +// SetHeader adds a key value pair to the options. The +// key-value pair will be part of the HTTP POST request +// headers. +func (o *PromptObjectOptions) SetHeader(key, value string) { + if o.headers == nil { + o.headers = make(map[string]string) + } + o.headers[http.CanonicalHeaderKey(key)] = value +} + +// toQueryValues - Convert the reqParams in Options to query string parameters. +func (o *PromptObjectOptions) toQueryValues() url.Values { + urlValues := make(url.Values) + if o.reqParams != nil { + for key, values := range o.reqParams { + for _, value := range values { + urlValues.Add(key, value) + } + } + } + + return urlValues +} diff --git a/vendor/github.com/minio/minio-go/v7/api-put-object-fan-out.go b/vendor/github.com/minio/minio-go/v7/api-put-object-fan-out.go index 0ae9142e1..3023b949c 100644 --- a/vendor/github.com/minio/minio-go/v7/api-put-object-fan-out.go +++ b/vendor/github.com/minio/minio-go/v7/api-put-object-fan-out.go @@ -85,7 +85,10 @@ func (c *Client) PutObjectFanOut(ctx context.Context, bucket string, fanOutData policy.SetEncryption(fanOutReq.SSE) // Set checksum headers if any. - policy.SetChecksum(fanOutReq.Checksum) + err := policy.SetChecksum(fanOutReq.Checksum) + if err != nil { + return nil, err + } url, formData, err := c.PresignedPostPolicy(ctx, policy) if err != nil { diff --git a/vendor/github.com/minio/minio-go/v7/api.go b/vendor/github.com/minio/minio-go/v7/api.go index 380ec4fde..88e8d4347 100644 --- a/vendor/github.com/minio/minio-go/v7/api.go +++ b/vendor/github.com/minio/minio-go/v7/api.go @@ -133,7 +133,7 @@ type Options struct { // Global constants. const ( libraryName = "minio-go" - libraryVersion = "v7.0.80" + libraryVersion = "v7.0.81" ) // User Agent should always following the below style. diff --git a/vendor/github.com/minio/minio-go/v7/functional_tests.go b/vendor/github.com/minio/minio-go/v7/functional_tests.go index c0180b36b..43383d134 100644 --- a/vendor/github.com/minio/minio-go/v7/functional_tests.go +++ b/vendor/github.com/minio/minio-go/v7/functional_tests.go @@ -160,7 +160,7 @@ func logError(testName, function string, args map[string]interface{}, startTime } else { logFailure(testName, function, args, startTime, alert, message, err) if !isRunOnFail() { - panic(err) + panic(fmt.Sprintf("Test failed with message: %s, err: %v", message, err)) } } } @@ -393,6 +393,42 @@ func getFuncNameLoc(caller int) string { return strings.TrimPrefix(runtime.FuncForPC(pc).Name(), "main.") } +type ClientConfig struct { + // MinIO client configuration + TraceOn bool // Turn on tracing of HTTP requests and responses to stderr + CredsV2 bool // Use V2 credentials if true, otherwise use v4 + TrailingHeaders bool // Send trailing headers in requests +} + +func NewClient(config ClientConfig) (*minio.Client, error) { + // Instantiate new MinIO client + var creds *credentials.Credentials + if config.CredsV2 { + creds = credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), "") + } else { + creds = credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), "") + } + opts := &minio.Options{ + Creds: creds, + Transport: createHTTPTransport(), + Secure: mustParseBool(os.Getenv(enableHTTPS)), + TrailingHeaders: config.TrailingHeaders, + } + client, err := minio.New(os.Getenv(serverEndpoint), opts) + if err != nil { + return nil, err + } + + if config.TraceOn { + client.TraceOn(os.Stderr) + } + + // Set user agent. + client.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + + return client, nil +} + // Tests bucket re-create errors. func testMakeBucketError() { region := "eu-central-1" @@ -407,27 +443,12 @@ func testMakeBucketError() { "region": region, } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - Transport: createHTTPTransport(), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -462,20 +483,12 @@ func testMetadataSizeLimit() { "objectName": "", "opts.UserMetadata": "", } - rand.Seed(startTime.Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - Transport: createHTTPTransport(), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client creation failed", err) return } - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -531,27 +544,12 @@ func testMakeBucketRegions() { "region": region, } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -598,27 +596,12 @@ func testPutObjectReadAt() { "opts": "objectContentType", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -697,27 +680,12 @@ func testListObjectVersions() { "recursive": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -817,27 +785,12 @@ func testStatObjectWithVersioning() { function := "StatObject" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -935,27 +888,12 @@ func testGetObjectWithVersioning() { function := "GetObject()" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -1075,27 +1013,12 @@ func testPutObjectWithVersioning() { function := "GetObject()" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -1223,28 +1146,12 @@ func testListMultipartUpload() { function := "GetObject()" args := map[string]interface{}{} - // Instantiate new minio client object. - opts := &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - } - c, err := minio.New(os.Getenv(serverEndpoint), opts) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - core, err := minio.NewCore(os.Getenv(serverEndpoint), opts) - if err != nil { - logError(testName, function, args, startTime, "", "MinIO core client object creation failed", err) - return - } - - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + core := minio.Core{Client: c} // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") @@ -1347,27 +1254,12 @@ func testCopyObjectWithVersioning() { function := "CopyObject()" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -1485,27 +1377,12 @@ func testConcurrentCopyObjectWithVersioning() { function := "CopyObject()" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -1646,27 +1523,12 @@ func testComposeObjectWithVersioning() { function := "ComposeObject()" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -1787,27 +1649,12 @@ func testRemoveObjectWithVersioning() { function := "DeleteObject()" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -1900,27 +1747,12 @@ func testRemoveObjectsWithVersioning() { function := "DeleteObjects()" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -1996,27 +1828,12 @@ func testObjectTaggingWithVersioning() { function := "{Get,Set,Remove}ObjectTagging()" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -2164,27 +1981,12 @@ func testPutObjectWithChecksums() { return } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -2230,7 +2032,7 @@ func testPutObjectWithChecksums() { h := test.cs.Hasher() h.Reset() - // Test with Wrong CRC. + // Test with a bad CRC - we haven't called h.Write(b), so this is a checksum of empty data meta[test.cs.Key()] = base64.StdEncoding.EncodeToString(h.Sum(nil)) args["metadata"] = meta args["range"] = "false" @@ -2350,28 +2152,12 @@ func testPutObjectWithTrailingChecksums() { return } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - TrailingHeaders: true, - }) + c, err := NewClient(ClientConfig{TrailingHeaders: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -2541,28 +2327,12 @@ func testPutMultipartObjectWithChecksums(trailing bool) { return } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - TrailingHeaders: trailing, - }) + c, err := NewClient(ClientConfig{TrailingHeaders: trailing}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -2620,7 +2390,7 @@ func testPutMultipartObjectWithChecksums(trailing bool) { cmpChecksum := func(got, want string) { if want != got { logError(testName, function, args, startTime, "", "checksum mismatch", fmt.Errorf("want %s, got %s", want, got)) - //fmt.Printf("want %s, got %s\n", want, got) + // fmt.Printf("want %s, got %s\n", want, got) return } } @@ -2741,25 +2511,12 @@ func testTrailingChecksums() { return } - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - TrailingHeaders: true, - }) + c, err := NewClient(ClientConfig{TrailingHeaders: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -2881,7 +2638,6 @@ func testTrailingChecksums() { test.ChecksumCRC32C = hashMultiPart(b, int(test.PO.PartSize), test.hasher) // Set correct CRC. - // c.TraceOn(os.Stderr) resp, err := c.PutObject(context.Background(), bucketName, objectName, bytes.NewReader(b), int64(bufSize), test.PO) if err != nil { logError(testName, function, args, startTime, "", "PutObject failed", err) @@ -2933,6 +2689,8 @@ func testTrailingChecksums() { delete(args, "metadata") } + + logSuccess(testName, function, args, startTime) } // Test PutObject with custom checksums. @@ -2952,25 +2710,12 @@ func testPutObjectWithAutomaticChecksums() { return } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - TrailingHeaders: true, - }) + c, err := NewClient(ClientConfig{TrailingHeaders: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -2997,8 +2742,6 @@ func testPutObjectWithAutomaticChecksums() { {header: "x-amz-checksum-crc32c", hasher: crc32.New(crc32.MakeTable(crc32.Castagnoli))}, } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) // defer c.TraceOff() for i, test := range tests { @@ -3108,20 +2851,12 @@ func testGetObjectAttributes() { return } - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - TrailingHeaders: true, - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{TrailingHeaders: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName err = c.MakeBucket( @@ -3315,19 +3050,12 @@ func testGetObjectAttributesSSECEncryption() { return } - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - TrailingHeaders: true, - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - Transport: createHTTPTransport(), - }) + c, err := NewClient(ClientConfig{TrailingHeaders: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName err = c.MakeBucket( @@ -3401,19 +3129,12 @@ func testGetObjectAttributesErrorCases() { return } - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - TrailingHeaders: true, - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{TrailingHeaders: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) unknownBucket := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-bucket-") unknownObject := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-object-") @@ -3657,27 +3378,12 @@ func testPutObjectWithMetadata() { return } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -3764,27 +3470,12 @@ func testPutObjectWithContentLanguage() { "opts": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -3834,27 +3525,12 @@ func testPutObjectStreaming() { "opts": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -3906,27 +3582,12 @@ func testGetObjectSeekEnd() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -4029,27 +3690,12 @@ func testGetObjectClosedTwice() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -4120,26 +3766,13 @@ func testRemoveObjectsContext() { "bucketName": "", } - // Seed random based on current tie. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Enable tracing, write to stdout. - // c.TraceOn(os.Stderr) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -4217,27 +3850,12 @@ func testRemoveMultipleObjects() { "bucketName": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - - // Enable tracing, write to stdout. - // c.TraceOn(os.Stderr) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -4301,27 +3919,12 @@ func testRemoveMultipleObjectsWithResult() { "bucketName": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - - // Enable tracing, write to stdout. - // c.TraceOn(os.Stderr) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -4437,27 +4040,12 @@ func testFPutObjectMultipart() { "opts": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -4543,27 +4131,12 @@ func testFPutObject() { "opts": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") location := "us-east-1" @@ -4713,27 +4286,13 @@ func testFPutObjectContext() { "fileName": "", "opts": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -4814,27 +4373,13 @@ func testFPutObjectContextV2() { "objectName": "", "opts": "minio.PutObjectOptions{ContentType:objectContentType}", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -4919,24 +4464,12 @@ func testPutObjectContext() { "opts": "", } - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Make a new bucket. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -4989,27 +4522,12 @@ func testGetObjectS3Zip() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{"x-minio-extract": true} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -5173,27 +4691,12 @@ func testGetObjectReadSeekFunctional() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -5343,27 +4846,12 @@ func testGetObjectReadAtFunctional() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -5521,27 +5009,12 @@ func testGetObjectReadAtWhenEOFWasReached() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -5641,27 +5114,12 @@ func testPresignedPostPolicy() { "policy": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") @@ -5689,50 +5147,22 @@ func testPresignedPostPolicy() { return } - // Save the data - _, err = c.PutObject(context.Background(), bucketName, objectName, bytes.NewReader(buf), int64(len(buf)), minio.PutObjectOptions{ContentType: "binary/octet-stream"}) - if err != nil { - logError(testName, function, args, startTime, "", "PutObject failed", err) - return - } - policy := minio.NewPostPolicy() - - if err := policy.SetBucket(""); err == nil { - logError(testName, function, args, startTime, "", "SetBucket did not fail for invalid conditions", err) - return - } - if err := policy.SetKey(""); err == nil { - logError(testName, function, args, startTime, "", "SetKey did not fail for invalid conditions", err) - return - } - if err := policy.SetExpires(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)); err == nil { - logError(testName, function, args, startTime, "", "SetExpires did not fail for invalid conditions", err) - return - } - if err := policy.SetContentType(""); err == nil { - logError(testName, function, args, startTime, "", "SetContentType did not fail for invalid conditions", err) - return - } - if err := policy.SetContentLengthRange(1024*1024, 1024); err == nil { - logError(testName, function, args, startTime, "", "SetContentLengthRange did not fail for invalid conditions", err) - return - } - if err := policy.SetUserMetadata("", ""); err == nil { - logError(testName, function, args, startTime, "", "SetUserMetadata did not fail for invalid conditions", err) - return - } - policy.SetBucket(bucketName) policy.SetKey(objectName) policy.SetExpires(time.Now().UTC().AddDate(0, 0, 10)) // expires in 10 days policy.SetContentType("binary/octet-stream") policy.SetContentLengthRange(10, 1024*1024) policy.SetUserMetadata(metadataKey, metadataValue) + policy.SetContentEncoding("gzip") // Add CRC32C checksum := minio.ChecksumCRC32C.ChecksumBytes(buf) - policy.SetChecksum(checksum) + err = policy.SetChecksum(checksum) + if err != nil { + logError(testName, function, args, startTime, "", "SetChecksum failed", err) + return + } args["policy"] = policy.String() @@ -5828,7 +5258,7 @@ func testPresignedPostPolicy() { expectedLocation := scheme + os.Getenv(serverEndpoint) + "/" + bucketName + "/" + objectName expectedLocationBucketDNS := scheme + bucketName + "." + os.Getenv(serverEndpoint) + "/" + objectName - if !strings.Contains(expectedLocation, "s3.amazonaws.com/") { + if !strings.Contains(expectedLocation, ".amazonaws.com/") { // Test when not against AWS S3. if val, ok := res.Header["Location"]; ok { if val[0] != expectedLocation && val[0] != expectedLocationBucketDNS { @@ -5840,9 +5270,194 @@ func testPresignedPostPolicy() { return } } - want := checksum.Encoded() - if got := res.Header.Get("X-Amz-Checksum-Crc32c"); got != want { - logError(testName, function, args, startTime, "", fmt.Sprintf("Want checksum %q, got %q", want, got), nil) + wantChecksumCrc32c := checksum.Encoded() + if got := res.Header.Get("X-Amz-Checksum-Crc32c"); got != wantChecksumCrc32c { + logError(testName, function, args, startTime, "", fmt.Sprintf("Want checksum %q, got %q", wantChecksumCrc32c, got), nil) + return + } + + // Ensure that when we subsequently GetObject, the checksum is returned + gopts := minio.GetObjectOptions{Checksum: true} + r, err := c.GetObject(context.Background(), bucketName, objectName, gopts) + if err != nil { + logError(testName, function, args, startTime, "", "GetObject failed", err) + return + } + st, err := r.Stat() + if err != nil { + logError(testName, function, args, startTime, "", "Stat failed", err) + return + } + if st.ChecksumCRC32C != wantChecksumCrc32c { + logError(testName, function, args, startTime, "", fmt.Sprintf("Want checksum %s, got %s", wantChecksumCrc32c, st.ChecksumCRC32C), nil) + return + } + + logSuccess(testName, function, args, startTime) +} + +// testPresignedPostPolicyWrongFile tests that when we have a policy with a checksum, we cannot POST the wrong file +func testPresignedPostPolicyWrongFile() { + // initialize logging params + startTime := time.Now() + testName := getFuncName() + function := "PresignedPostPolicy(policy)" + args := map[string]interface{}{ + "policy": "", + } + + c, err := NewClient(ClientConfig{}) + if err != nil { + logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) + return + } + + // Generate a new random bucket name. + bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") + + // Make a new bucket in 'us-east-1' (source bucket). + err = c.MakeBucket(context.Background(), bucketName, minio.MakeBucketOptions{Region: "us-east-1"}) + if err != nil { + logError(testName, function, args, startTime, "", "MakeBucket failed", err) + return + } + + defer cleanupBucket(bucketName, c) + + // Generate 33K of data. + reader := getDataReader("datafile-33-kB") + defer reader.Close() + + objectName := randString(60, rand.NewSource(time.Now().UnixNano()), "") + // Azure requires the key to not start with a number + metadataKey := randString(60, rand.NewSource(time.Now().UnixNano()), "user") + metadataValue := randString(60, rand.NewSource(time.Now().UnixNano()), "") + + buf, err := io.ReadAll(reader) + if err != nil { + logError(testName, function, args, startTime, "", "ReadAll failed", err) + return + } + + policy := minio.NewPostPolicy() + policy.SetBucket(bucketName) + policy.SetKey(objectName) + policy.SetExpires(time.Now().UTC().AddDate(0, 0, 10)) // expires in 10 days + policy.SetContentType("binary/octet-stream") + policy.SetContentLengthRange(10, 1024*1024) + policy.SetUserMetadata(metadataKey, metadataValue) + + // Add CRC32C of the 33kB file that the policy will explicitly allow. + checksum := minio.ChecksumCRC32C.ChecksumBytes(buf) + err = policy.SetChecksum(checksum) + if err != nil { + logError(testName, function, args, startTime, "", "SetChecksum failed", err) + return + } + + args["policy"] = policy.String() + + presignedPostPolicyURL, formData, err := c.PresignedPostPolicy(context.Background(), policy) + if err != nil { + logError(testName, function, args, startTime, "", "PresignedPostPolicy failed", err) + return + } + + // At this stage, we have a policy that allows us to upload datafile-33-kB. + // Test that uploading datafile-10-kB, with a different checksum, fails as expected + filePath := getMintDataDirFilePath("datafile-10-kB") + if filePath == "" { + // Make a temp file with 10 KB data. + file, err := os.CreateTemp(os.TempDir(), "PresignedPostPolicyTest") + if err != nil { + logError(testName, function, args, startTime, "", "TempFile creation failed", err) + return + } + if _, err = io.Copy(file, getDataReader("datafile-10-kB")); err != nil { + logError(testName, function, args, startTime, "", "Copy failed", err) + return + } + if err = file.Close(); err != nil { + logError(testName, function, args, startTime, "", "File Close failed", err) + return + } + filePath = file.Name() + } + fileReader := getDataReader("datafile-10-kB") + defer fileReader.Close() + buf10k, err := io.ReadAll(fileReader) + if err != nil { + logError(testName, function, args, startTime, "", "ReadAll failed", err) + return + } + otherChecksum := minio.ChecksumCRC32C.ChecksumBytes(buf10k) + + var formBuf bytes.Buffer + writer := multipart.NewWriter(&formBuf) + for k, v := range formData { + if k == "x-amz-checksum-crc32c" { + v = otherChecksum.Encoded() + } + writer.WriteField(k, v) + } + + // Add file to post request + f, err := os.Open(filePath) + defer f.Close() + if err != nil { + logError(testName, function, args, startTime, "", "File open failed", err) + return + } + w, err := writer.CreateFormFile("file", filePath) + if err != nil { + logError(testName, function, args, startTime, "", "CreateFormFile failed", err) + return + } + _, err = io.Copy(w, f) + if err != nil { + logError(testName, function, args, startTime, "", "Copy failed", err) + return + } + writer.Close() + + httpClient := &http.Client{ + Timeout: 30 * time.Second, + Transport: createHTTPTransport(), + } + args["url"] = presignedPostPolicyURL.String() + + req, err := http.NewRequest(http.MethodPost, presignedPostPolicyURL.String(), bytes.NewReader(formBuf.Bytes())) + if err != nil { + logError(testName, function, args, startTime, "", "HTTP request failed", err) + return + } + + req.Header.Set("Content-Type", writer.FormDataContentType()) + + // Make the POST request with the form data. + res, err := httpClient.Do(req) + if err != nil { + logError(testName, function, args, startTime, "", "HTTP request failed", err) + return + } + defer res.Body.Close() + if res.StatusCode != http.StatusForbidden { + logError(testName, function, args, startTime, "", "HTTP request unexpected status", errors.New(res.Status)) + return + } + + // Read the response body, ensure it has checksum failure message + resBody, err := io.ReadAll(res.Body) + if err != nil { + logError(testName, function, args, startTime, "", "ReadAll failed", err) + return + } + + // Normalize the response body, because S3 uses quotes around the policy condition components + // in the error message, MinIO does not. + resBodyStr := strings.ReplaceAll(string(resBody), `"`, "") + if !strings.Contains(resBodyStr, "Policy Condition failed: [eq, $x-amz-checksum-crc32c, aHnJMw==]") { + logError(testName, function, args, startTime, "", "Unexpected response body", errors.New(resBodyStr)) return } @@ -5857,27 +5472,12 @@ func testCopyObject() { function := "CopyObject(dst, src)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") @@ -6052,27 +5652,12 @@ func testSSECEncryptedGetObjectReadSeekFunctional() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -6235,27 +5820,12 @@ func testSSES3EncryptedGetObjectReadSeekFunctional() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -6416,27 +5986,12 @@ func testSSECEncryptedGetObjectReadAtFunctional() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -6600,27 +6155,12 @@ func testSSES3EncryptedGetObjectReadAtFunctional() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -6785,27 +6325,13 @@ func testSSECEncryptionPutGet() { "objectName": "", "sse": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -6895,27 +6421,13 @@ func testSSECEncryptionFPut() { "contentType": "", "sse": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -7018,27 +6530,13 @@ func testSSES3EncryptionPutGet() { "objectName": "", "sse": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -7126,27 +6624,13 @@ func testSSES3EncryptionFPut() { "contentType": "", "sse": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -7255,26 +6739,12 @@ func testBucketNotification() { return } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable to debug - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - bucketName := os.Getenv("NOTIFY_BUCKET") args["bucketName"] = bucketName @@ -7350,26 +6820,12 @@ func testFunctional() { functionAll := "" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, nil, startTime, "", "MinIO client object creation failed", err) return } - // Enable to debug - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") @@ -8029,24 +7485,12 @@ func testGetObjectModified() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Make a new bucket. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -8125,24 +7569,12 @@ func testPutObjectUploadSeekedObject() { "contentType": "binary/octet-stream", } - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Make a new bucket. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -8245,27 +7677,12 @@ func testMakeBucketErrorV2() { "region": "eu-west-1", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") region := "eu-west-1" @@ -8305,27 +7722,12 @@ func testGetObjectClosedTwiceV2() { "region": "eu-west-1", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -8396,27 +7798,12 @@ func testFPutObjectV2() { "opts": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -8557,27 +7944,12 @@ func testMakeBucketRegionsV2() { "region": "eu-west-1", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -8620,27 +7992,12 @@ func testGetObjectReadSeekFunctionalV2() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -8775,27 +8132,12 @@ func testGetObjectReadAtFunctionalV2() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -8937,27 +8279,12 @@ func testCopyObjectV2() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") @@ -9156,13 +8483,7 @@ func testComposeObjectErrorCasesV2() { function := "ComposeObject(destination, sourceList)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9254,13 +8575,7 @@ func testCompose10KSourcesV2() { function := "ComposeObject(destination, sourceList)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9276,13 +8591,7 @@ func testEncryptedEmptyObject() { function := "PutObject(bucketName, objectName, reader, objectSize, opts)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return @@ -9430,7 +8739,7 @@ func testEncryptedCopyObjectWrapper(c *minio.Client, bucketName string, sseSrc, dstEncryption = sseDst } // 3. get copied object and check if content is equal - coreClient := minio.Core{c} + coreClient := minio.Core{Client: c} reader, _, _, err := coreClient.GetObject(context.Background(), bucketName, "dstObject", minio.GetObjectOptions{ServerSideEncryption: dstEncryption}) if err != nil { logError(testName, function, args, startTime, "", "GetObject failed", err) @@ -9537,13 +8846,7 @@ func testUnencryptedToSSECCopyObject() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9552,7 +8855,6 @@ func testUnencryptedToSSECCopyObject() { bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") sseDst := encrypt.DefaultPBKDF([]byte("correct horse battery staple"), []byte(bucketName+"dstObject")) - // c.TraceOn(os.Stderr) testEncryptedCopyObjectWrapper(c, bucketName, nil, sseDst) } @@ -9564,13 +8866,7 @@ func testUnencryptedToSSES3CopyObject() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9580,7 +8876,6 @@ func testUnencryptedToSSES3CopyObject() { var sseSrc encrypt.ServerSide sseDst := encrypt.NewSSE() - // c.TraceOn(os.Stderr) testEncryptedCopyObjectWrapper(c, bucketName, sseSrc, sseDst) } @@ -9592,13 +8887,7 @@ func testUnencryptedToUnencryptedCopyObject() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9607,7 +8896,6 @@ func testUnencryptedToUnencryptedCopyObject() { bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") var sseSrc, sseDst encrypt.ServerSide - // c.TraceOn(os.Stderr) testEncryptedCopyObjectWrapper(c, bucketName, sseSrc, sseDst) } @@ -9619,13 +8907,7 @@ func testEncryptedSSECToSSECCopyObject() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9635,7 +8917,6 @@ func testEncryptedSSECToSSECCopyObject() { sseSrc := encrypt.DefaultPBKDF([]byte("correct horse battery staple"), []byte(bucketName+"srcObject")) sseDst := encrypt.DefaultPBKDF([]byte("correct horse battery staple"), []byte(bucketName+"dstObject")) - // c.TraceOn(os.Stderr) testEncryptedCopyObjectWrapper(c, bucketName, sseSrc, sseDst) } @@ -9647,13 +8928,7 @@ func testEncryptedSSECToSSES3CopyObject() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9663,7 +8938,6 @@ func testEncryptedSSECToSSES3CopyObject() { sseSrc := encrypt.DefaultPBKDF([]byte("correct horse battery staple"), []byte(bucketName+"srcObject")) sseDst := encrypt.NewSSE() - // c.TraceOn(os.Stderr) testEncryptedCopyObjectWrapper(c, bucketName, sseSrc, sseDst) } @@ -9675,13 +8949,7 @@ func testEncryptedSSECToUnencryptedCopyObject() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9691,7 +8959,6 @@ func testEncryptedSSECToUnencryptedCopyObject() { sseSrc := encrypt.DefaultPBKDF([]byte("correct horse battery staple"), []byte(bucketName+"srcObject")) var sseDst encrypt.ServerSide - // c.TraceOn(os.Stderr) testEncryptedCopyObjectWrapper(c, bucketName, sseSrc, sseDst) } @@ -9703,13 +8970,7 @@ func testEncryptedSSES3ToSSECCopyObject() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9719,7 +8980,6 @@ func testEncryptedSSES3ToSSECCopyObject() { sseSrc := encrypt.NewSSE() sseDst := encrypt.DefaultPBKDF([]byte("correct horse battery staple"), []byte(bucketName+"dstObject")) - // c.TraceOn(os.Stderr) testEncryptedCopyObjectWrapper(c, bucketName, sseSrc, sseDst) } @@ -9731,13 +8991,7 @@ func testEncryptedSSES3ToSSES3CopyObject() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9747,7 +9001,6 @@ func testEncryptedSSES3ToSSES3CopyObject() { sseSrc := encrypt.NewSSE() sseDst := encrypt.NewSSE() - // c.TraceOn(os.Stderr) testEncryptedCopyObjectWrapper(c, bucketName, sseSrc, sseDst) } @@ -9759,13 +9012,7 @@ func testEncryptedSSES3ToUnencryptedCopyObject() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9775,7 +9022,6 @@ func testEncryptedSSES3ToUnencryptedCopyObject() { sseSrc := encrypt.NewSSE() var sseDst encrypt.ServerSide - // c.TraceOn(os.Stderr) testEncryptedCopyObjectWrapper(c, bucketName, sseSrc, sseDst) } @@ -9787,13 +9033,7 @@ func testEncryptedCopyObjectV2() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9803,7 +9043,6 @@ func testEncryptedCopyObjectV2() { sseSrc := encrypt.DefaultPBKDF([]byte("correct horse battery staple"), []byte(bucketName+"srcObject")) sseDst := encrypt.DefaultPBKDF([]byte("correct horse battery staple"), []byte(bucketName+"dstObject")) - // c.TraceOn(os.Stderr) testEncryptedCopyObjectWrapper(c, bucketName, sseSrc, sseDst) } @@ -9814,13 +9053,7 @@ func testDecryptedCopyObject() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9874,26 +9107,14 @@ func testSSECMultipartEncryptedToSSECCopyObjectPart() { function := "CopyObjectPart(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - client, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + client, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return } // Instantiate new core client object. - c := minio.Core{client} - - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + c := minio.Core{Client: client} // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test") @@ -10072,26 +9293,14 @@ func testSSECEncryptedToSSECCopyObjectPart() { function := "CopyObjectPart(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - client, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + client, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return } // Instantiate new core client object. - c := minio.Core{client} - - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + c := minio.Core{Client: client} // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test") @@ -10250,26 +9459,14 @@ func testSSECEncryptedToUnencryptedCopyPart() { function := "CopyObjectPart(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - client, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + client, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return } // Instantiate new core client object. - c := minio.Core{client} - - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + c := minio.Core{Client: client} // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test") @@ -10427,26 +9624,14 @@ func testSSECEncryptedToSSES3CopyObjectPart() { function := "CopyObjectPart(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - client, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + client, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return } // Instantiate new core client object. - c := minio.Core{client} - - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + c := minio.Core{Client: client} // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test") @@ -10607,26 +9792,14 @@ func testUnencryptedToSSECCopyObjectPart() { function := "CopyObjectPart(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - client, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + client, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return } // Instantiate new core client object. - c := minio.Core{client} - - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + c := minio.Core{Client: client} // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test") @@ -10782,26 +9955,14 @@ func testUnencryptedToUnencryptedCopyPart() { function := "CopyObjectPart(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - client, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + client, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return } // Instantiate new core client object. - c := minio.Core{client} - - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + c := minio.Core{Client: client} // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test") @@ -10953,26 +10114,14 @@ func testUnencryptedToSSES3CopyObjectPart() { function := "CopyObjectPart(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - client, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + client, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return } // Instantiate new core client object. - c := minio.Core{client} - - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + c := minio.Core{Client: client} // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test") @@ -11126,26 +10275,14 @@ func testSSES3EncryptedToSSECCopyObjectPart() { function := "CopyObjectPart(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - client, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + client, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return } // Instantiate new core client object. - c := minio.Core{client} - - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + c := minio.Core{Client: client} // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test") @@ -11302,26 +10439,14 @@ func testSSES3EncryptedToUnencryptedCopyPart() { function := "CopyObjectPart(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - client, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + client, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return } // Instantiate new core client object. - c := minio.Core{client} - - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + c := minio.Core{Client: client} // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test") @@ -11474,26 +10599,14 @@ func testSSES3EncryptedToSSES3CopyObjectPart() { function := "CopyObjectPart(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - client, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + client, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return } // Instantiate new core client object. - c := minio.Core{client} - - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + c := minio.Core{Client: client} // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test") @@ -11648,19 +10761,12 @@ func testUserMetadataCopying() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // c.TraceOn(os.Stderr) testUserMetadataCopyingWrapper(c) } @@ -11825,19 +10931,12 @@ func testUserMetadataCopyingV2() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client v2 object creation failed", err) return } - // c.TraceOn(os.Stderr) testUserMetadataCopyingWrapper(c) } @@ -11848,13 +10947,7 @@ func testStorageClassMetadataPutObject() { args := map[string]interface{}{} testName := getFuncName() - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return @@ -11936,13 +11029,7 @@ func testStorageClassInvalidMetadataPutObject() { args := map[string]interface{}{} testName := getFuncName() - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return @@ -11979,13 +11066,7 @@ func testStorageClassMetadataCopyObject() { args := map[string]interface{}{} testName := getFuncName() - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - Transport: createHTTPTransport(), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return @@ -12106,27 +11187,12 @@ func testPutObjectNoLengthV2() { "opts": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { - logError(testName, function, args, startTime, "", "MinIO client v2 object creation failed", err) + logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -12182,27 +11248,12 @@ func testPutObjectsUnknownV2() { "opts": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { - logError(testName, function, args, startTime, "", "MinIO client v2 object creation failed", err) + logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -12273,27 +11324,12 @@ func testPutObject0ByteV2() { "opts": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { - logError(testName, function, args, startTime, "", "MinIO client v2 object creation failed", err) + logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -12338,13 +11374,7 @@ func testComposeObjectErrorCases() { function := "ComposeObject(destination, sourceList)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return @@ -12361,13 +11391,7 @@ func testCompose10KSources() { function := "ComposeObject(destination, sourceList)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return @@ -12385,26 +11409,12 @@ func testFunctionalV2() { functionAll := "" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - Transport: createHTTPTransport(), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client v2 object creation failed", err) return } - // Enable to debug - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") location := "us-east-1" @@ -12838,27 +11848,13 @@ func testGetObjectContext() { "bucketName": "", "objectName": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client v4 object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -12941,27 +11937,13 @@ func testFGetObjectContext() { "objectName": "", "fileName": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client v4 object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -13033,24 +12015,12 @@ func testGetObjectRanges() { defer cancel() rng := rand.NewSource(time.Now().UnixNano()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client v4 object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rng, "minio-go-test-") args["bucketName"] = bucketName @@ -13140,27 +12110,13 @@ func testGetObjectACLContext() { "bucketName": "", "objectName": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client v4 object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -13318,24 +12274,12 @@ func testPutObjectContextV2() { "size": "", "opts": "", } - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { - logError(testName, function, args, startTime, "", "MinIO client v2 object creation failed", err) + logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Make a new bucket. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -13390,27 +12334,13 @@ func testGetObjectContextV2() { "bucketName": "", "objectName": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { - logError(testName, function, args, startTime, "", "MinIO client v2 object creation failed", err) + logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -13491,27 +12421,13 @@ func testFGetObjectContextV2() { "objectName": "", "fileName": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { - logError(testName, function, args, startTime, "", "MinIO client v2 object creation failed", err) + logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -13580,27 +12496,13 @@ func testListObjects() { "objectPrefix": "", "recursive": "true", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client v4 object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -13684,24 +12586,12 @@ func testCors() { "cors": "", } - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Create or reuse a bucket that will get cors settings applied to it and deleted when done bucketName := os.Getenv("MINIO_GO_TEST_BUCKET_CORS") if bucketName == "" { @@ -14420,24 +13310,12 @@ func testCorsSetGetDelete() { "cors": "", } - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -14519,27 +13397,13 @@ func testRemoveObjects() { "objectPrefix": "", "recursive": "true", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client v4 object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -14653,27 +13517,13 @@ func testGetBucketTagging() { args := map[string]interface{}{ "bucketName": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client v4 object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -14709,27 +13559,13 @@ func testSetBucketTagging() { "bucketName": "", "tags": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client v4 object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -14795,27 +13631,13 @@ func testRemoveBucketTagging() { args := map[string]interface{}{ "bucketName": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client v4 object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -14961,6 +13783,7 @@ func main() { testGetObjectReadAtFunctional() testGetObjectReadAtWhenEOFWasReached() testPresignedPostPolicy() + testPresignedPostPolicyWrongFile() testCopyObject() testComposeObjectErrorCases() testCompose10KSources() diff --git a/vendor/github.com/minio/minio-go/v7/pkg/credentials/sts_web_identity.go b/vendor/github.com/minio/minio-go/v7/pkg/credentials/sts_web_identity.go index f1c76c78e..787f0a38d 100644 --- a/vendor/github.com/minio/minio-go/v7/pkg/credentials/sts_web_identity.go +++ b/vendor/github.com/minio/minio-go/v7/pkg/credentials/sts_web_identity.go @@ -58,9 +58,10 @@ type WebIdentityResult struct { // WebIdentityToken - web identity token with expiry. type WebIdentityToken struct { - Token string - AccessToken string - Expiry int + Token string + AccessToken string + RefreshToken string + Expiry int } // A STSWebIdentity retrieves credentials from MinIO service, and keeps track if diff --git a/vendor/github.com/minio/minio-go/v7/post-policy.go b/vendor/github.com/minio/minio-go/v7/post-policy.go index 19687e027..26bf441b5 100644 --- a/vendor/github.com/minio/minio-go/v7/post-policy.go +++ b/vendor/github.com/minio/minio-go/v7/post-policy.go @@ -85,7 +85,7 @@ func (p *PostPolicy) SetExpires(t time.Time) error { // SetKey - Sets an object name for the policy based upload. func (p *PostPolicy) SetKey(key string) error { - if strings.TrimSpace(key) == "" || key == "" { + if strings.TrimSpace(key) == "" { return errInvalidArgument("Object name is empty.") } policyCond := policyCondition{ @@ -118,7 +118,7 @@ func (p *PostPolicy) SetKeyStartsWith(keyStartsWith string) error { // SetBucket - Sets bucket at which objects will be uploaded to. func (p *PostPolicy) SetBucket(bucketName string) error { - if strings.TrimSpace(bucketName) == "" || bucketName == "" { + if strings.TrimSpace(bucketName) == "" { return errInvalidArgument("Bucket name is empty.") } policyCond := policyCondition{ @@ -135,7 +135,7 @@ func (p *PostPolicy) SetBucket(bucketName string) error { // SetCondition - Sets condition for credentials, date and algorithm func (p *PostPolicy) SetCondition(matchType, condition, value string) error { - if strings.TrimSpace(value) == "" || value == "" { + if strings.TrimSpace(value) == "" { return errInvalidArgument("No value specified for condition") } @@ -156,7 +156,7 @@ func (p *PostPolicy) SetCondition(matchType, condition, value string) error { // SetTagging - Sets tagging for the object for this policy based upload. func (p *PostPolicy) SetTagging(tagging string) error { - if strings.TrimSpace(tagging) == "" || tagging == "" { + if strings.TrimSpace(tagging) == "" { return errInvalidArgument("No tagging specified.") } _, err := tags.ParseObjectXML(strings.NewReader(tagging)) @@ -178,7 +178,7 @@ func (p *PostPolicy) SetTagging(tagging string) error { // SetContentType - Sets content-type of the object for this policy // based upload. func (p *PostPolicy) SetContentType(contentType string) error { - if strings.TrimSpace(contentType) == "" || contentType == "" { + if strings.TrimSpace(contentType) == "" { return errInvalidArgument("No content type specified.") } policyCond := policyCondition{ @@ -211,7 +211,7 @@ func (p *PostPolicy) SetContentTypeStartsWith(contentTypeStartsWith string) erro // SetContentDisposition - Sets content-disposition of the object for this policy func (p *PostPolicy) SetContentDisposition(contentDisposition string) error { - if strings.TrimSpace(contentDisposition) == "" || contentDisposition == "" { + if strings.TrimSpace(contentDisposition) == "" { return errInvalidArgument("No content disposition specified.") } policyCond := policyCondition{ @@ -226,27 +226,44 @@ func (p *PostPolicy) SetContentDisposition(contentDisposition string) error { return nil } +// SetContentEncoding - Sets content-encoding of the object for this policy +func (p *PostPolicy) SetContentEncoding(contentEncoding string) error { + if strings.TrimSpace(contentEncoding) == "" { + return errInvalidArgument("No content encoding specified.") + } + policyCond := policyCondition{ + matchType: "eq", + condition: "$Content-Encoding", + value: contentEncoding, + } + if err := p.addNewPolicy(policyCond); err != nil { + return err + } + p.formData["Content-Encoding"] = contentEncoding + return nil +} + // SetContentLengthRange - Set new min and max content length // condition for all incoming uploads. -func (p *PostPolicy) SetContentLengthRange(min, max int64) error { - if min > max { +func (p *PostPolicy) SetContentLengthRange(minLen, maxLen int64) error { + if minLen > maxLen { return errInvalidArgument("Minimum limit is larger than maximum limit.") } - if min < 0 { + if minLen < 0 { return errInvalidArgument("Minimum limit cannot be negative.") } - if max <= 0 { + if maxLen <= 0 { return errInvalidArgument("Maximum limit cannot be non-positive.") } - p.contentLengthRange.min = min - p.contentLengthRange.max = max + p.contentLengthRange.min = minLen + p.contentLengthRange.max = maxLen return nil } // SetSuccessActionRedirect - Sets the redirect success url of the object for this policy // based upload. func (p *PostPolicy) SetSuccessActionRedirect(redirect string) error { - if strings.TrimSpace(redirect) == "" || redirect == "" { + if strings.TrimSpace(redirect) == "" { return errInvalidArgument("Redirect is empty") } policyCond := policyCondition{ @@ -264,7 +281,7 @@ func (p *PostPolicy) SetSuccessActionRedirect(redirect string) error { // SetSuccessStatusAction - Sets the status success code of the object for this policy // based upload. func (p *PostPolicy) SetSuccessStatusAction(status string) error { - if strings.TrimSpace(status) == "" || status == "" { + if strings.TrimSpace(status) == "" { return errInvalidArgument("Status is empty") } policyCond := policyCondition{ @@ -282,10 +299,10 @@ func (p *PostPolicy) SetSuccessStatusAction(status string) error { // SetUserMetadata - Set user metadata as a key/value couple. // Can be retrieved through a HEAD request or an event. func (p *PostPolicy) SetUserMetadata(key, value string) error { - if strings.TrimSpace(key) == "" || key == "" { + if strings.TrimSpace(key) == "" { return errInvalidArgument("Key is empty") } - if strings.TrimSpace(value) == "" || value == "" { + if strings.TrimSpace(value) == "" { return errInvalidArgument("Value is empty") } headerName := fmt.Sprintf("x-amz-meta-%s", key) @@ -304,7 +321,7 @@ func (p *PostPolicy) SetUserMetadata(key, value string) error { // SetUserMetadataStartsWith - Set how an user metadata should starts with. // Can be retrieved through a HEAD request or an event. func (p *PostPolicy) SetUserMetadataStartsWith(key, value string) error { - if strings.TrimSpace(key) == "" || key == "" { + if strings.TrimSpace(key) == "" { return errInvalidArgument("Key is empty") } headerName := fmt.Sprintf("x-amz-meta-%s", key) @@ -321,11 +338,29 @@ func (p *PostPolicy) SetUserMetadataStartsWith(key, value string) error { } // SetChecksum sets the checksum of the request. -func (p *PostPolicy) SetChecksum(c Checksum) { +func (p *PostPolicy) SetChecksum(c Checksum) error { if c.IsSet() { p.formData[amzChecksumAlgo] = c.Type.String() p.formData[c.Type.Key()] = c.Encoded() + + policyCond := policyCondition{ + matchType: "eq", + condition: fmt.Sprintf("$%s", amzChecksumAlgo), + value: c.Type.String(), + } + if err := p.addNewPolicy(policyCond); err != nil { + return err + } + policyCond = policyCondition{ + matchType: "eq", + condition: fmt.Sprintf("$%s", c.Type.Key()), + value: c.Encoded(), + } + if err := p.addNewPolicy(policyCond); err != nil { + return err + } } + return nil } // SetEncryption - sets encryption headers for POST API diff --git a/vendor/github.com/minio/minio-go/v7/retry-continous.go b/vendor/github.com/minio/minio-go/v7/retry-continous.go index bfeea95f3..81fcf16f1 100644 --- a/vendor/github.com/minio/minio-go/v7/retry-continous.go +++ b/vendor/github.com/minio/minio-go/v7/retry-continous.go @@ -20,7 +20,7 @@ package minio import "time" // newRetryTimerContinous creates a timer with exponentially increasing delays forever. -func (c *Client) newRetryTimerContinous(unit, cap time.Duration, jitter float64, doneCh chan struct{}) <-chan int { +func (c *Client) newRetryTimerContinous(baseSleep, maxSleep time.Duration, jitter float64, doneCh chan struct{}) <-chan int { attemptCh := make(chan int) // normalize jitter to the range [0, 1.0] @@ -39,10 +39,10 @@ func (c *Client) newRetryTimerContinous(unit, cap time.Duration, jitter float64, if attempt > maxAttempt { attempt = maxAttempt } - // sleep = random_between(0, min(cap, base * 2 ** attempt)) - sleep := unit * time.Duration(1< cap { - sleep = cap + // sleep = random_between(0, min(maxSleep, base * 2 ** attempt)) + sleep := baseSleep * time.Duration(1< maxSleep { + sleep = maxSleep } if jitter != NoJitter { sleep -= time.Duration(c.random.Float64() * float64(sleep) * jitter) diff --git a/vendor/github.com/minio/minio-go/v7/retry.go b/vendor/github.com/minio/minio-go/v7/retry.go index d15eb5901..4cc45920c 100644 --- a/vendor/github.com/minio/minio-go/v7/retry.go +++ b/vendor/github.com/minio/minio-go/v7/retry.go @@ -45,7 +45,7 @@ var DefaultRetryCap = time.Second // newRetryTimer creates a timer with exponentially increasing // delays until the maximum retry attempts are reached. -func (c *Client) newRetryTimer(ctx context.Context, maxRetry int, unit, cap time.Duration, jitter float64) <-chan int { +func (c *Client) newRetryTimer(ctx context.Context, maxRetry int, baseSleep, maxSleep time.Duration, jitter float64) <-chan int { attemptCh := make(chan int) // computes the exponential backoff duration according to @@ -59,10 +59,10 @@ func (c *Client) newRetryTimer(ctx context.Context, maxRetry int, unit, cap time jitter = MaxJitter } - // sleep = random_between(0, min(cap, base * 2 ** attempt)) - sleep := unit * time.Duration(1< cap { - sleep = cap + // sleep = random_between(0, min(maxSleep, base * 2 ** attempt)) + sleep := baseSleep * time.Duration(1< maxSleep { + sleep = maxSleep } if jitter != NoJitter { sleep -= time.Duration(c.random.Float64() * float64(sleep) * jitter) diff --git a/vendor/modules.txt b/vendor/modules.txt index 4c57a75de..c5f13d968 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -488,7 +488,7 @@ github.com/miekg/dns # github.com/minio/md5-simd v1.1.2 ## explicit; go 1.14 github.com/minio/md5-simd -# github.com/minio/minio-go/v7 v7.0.80 +# github.com/minio/minio-go/v7 v7.0.81 ## explicit; go 1.22 github.com/minio/minio-go/v7 github.com/minio/minio-go/v7/pkg/cors From dbef5ee03b61d630b46696cfd04c08fcd5b702ce Mon Sep 17 00:00:00 2001 From: Phil Hagelberg Date: Mon, 2 Dec 2024 01:47:05 -0800 Subject: [PATCH 18/26] [chore] Replace Semaphore recommendation with Pinafore. (#3586) Neither Semaphore nor Pinafore are under active development, but Semaphore has archived its repository while Pinafore still gets occasional minor maintenance. Enafore has newer features, but it has accessibility bugs affecting screen readers that prevent it from being recommended at this time. --- CONTRIBUTING.md | 10 +++++----- README.md | 2 +- docs/faq.md | 2 +- .../getting_started/reverse_proxy/websocket.md | 2 +- docs/locales/zh/faq.md | 2 +- .../getting_started/reverse_proxy/websocket.md | 2 +- docs/locales/zh/repo/CONTRIBUTING.md | 10 +++++----- docs/locales/zh/repo/README.md | 2 +- web/template/index_apps.tmpl | 18 +++++++++--------- 9 files changed, 25 insertions(+), 25 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6b52d6b59..4fa148f37 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ These contribution guidelines were adapted from / inspired by those of Gitea (ht - [Finding your way around the code](#finding-your-way-around-the-code) - [Style / Linting / Formatting](#style--linting--formatting) - [Testing](#testing) - - [Standalone Testrig with Semaphore](#standalone-testrig-with-semaphore) + - [Standalone Testrig with Pinafore](#standalone-testrig-with-pinafore) - [Running automated tests](#running-automated-tests) - [SQLite](#sqlite) - [Postgres](#postgres) @@ -401,9 +401,9 @@ GoToSocial provides a [testrig](https://github.com/superseriousbusiness/gotosoci One thing that *isn't* mocked is the Database interface because it's just easier to use an in-memory SQLite database than to mock everything out. -#### Standalone Testrig with Semaphore +#### Standalone Testrig with Pinafore -You can launch a testrig as a standalone server running at localhost, which you can connect to using something like [Semaphore](https://github.com/NickColley/semaphore/). +You can launch a testrig as a standalone server running at localhost, which you can connect to using something like [Pinafore](https://github.com/nolanlawson/pinafore/). To do this, first build the gotosocial binary with `DEBUG=1 ./scripts/build.sh`. @@ -413,14 +413,14 @@ Then, launch the testrig with the `DEBUG` environment variable set by invoking t DEBUG=1 ./gotosocial testrig start ``` -To run Semaphore locally in dev mode, first clone the [Semaphore](https://github.com/NickColley/semaphore/) repository, and then run the following commands in the cloned directory: +To run Pinafore locally in dev mode, first clone the [Pinafore](https://github.com/nolanlawson/pinafore/) repository, and then run the following commands in the cloned directory: ```bash yarn # install dependencies yarn run dev ``` -The Semaphore instance will start running on `localhost:4002`. +The Pinafore instance will start running on `localhost:4002`. To connect to the testrig, navigate to `http://localhost:4002` and enter your instance name as `localhost:8080`. diff --git a/README.md b/README.md index c37af7533..3f9f357e2 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ The Mastodon API has become the de facto standard for client communication with Though most apps that implement the Mastodon API should work, GoToSocial is tested and works reliably with beautiful apps like: * [Tusky](https://tusky.app/) for Android -* [Semaphore](https://semaphore.social/) in the browser +* [Pinafore](https://pinafore.social/) in the browser * [Feditext](https://github.com/feditext/feditext) (beta) on iOS, iPadOS and macOS If you've used Mastodon with a third-party app before, you'll find using GoToSocial a breeze. diff --git a/docs/faq.md b/docs/faq.md index 521c97531..72fed557d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -2,7 +2,7 @@ ## Where's the user interface? -GoToSocial is just a bare server for the most part and is designed to be used thru external applications. [Semaphore](https://semaphore.social/) in the browser, [Tusky](https://tusky.app/) for Android and [Feditext](https://github.com/feditext/feditext) for iOS, iPadOS and macOS are the best-supported. Anything that supports the Mastodon API should work, other than the features GoToSocial doesn't yet have. Permalinks and profile pages are served directly through GoToSocial as well as the settings panel, but most interaction goes through the apps. +GoToSocial is just a bare server for the most part and is designed to be used thru external applications. [Pinafore](https://pinafore.social/) in the browser, [Tusky](https://tusky.app/) for Android and [Feditext](https://github.com/feditext/feditext) for iOS, iPadOS and macOS are the best-supported. Anything that supports the Mastodon API should work, other than the features GoToSocial doesn't yet have. Permalinks and profile pages are served directly through GoToSocial as well as the settings panel, but most interaction goes through the apps. ## Why aren't my posts showing up on my profile page? diff --git a/docs/getting_started/reverse_proxy/websocket.md b/docs/getting_started/reverse_proxy/websocket.md index ec7c107a9..68d28bc5c 100644 --- a/docs/getting_started/reverse_proxy/websocket.md +++ b/docs/getting_started/reverse_proxy/websocket.md @@ -1,6 +1,6 @@ # WebSocket -GoToSocial uses the secure [WebSocket protocol](https://en.wikipedia.org/wiki/WebSocket) (aka `wss`) to allow for streaming updates of statuses and notifications via client apps like Semaphore. +GoToSocial uses the secure [WebSocket protocol](https://en.wikipedia.org/wiki/WebSocket) (aka `wss`) to allow for streaming updates of statuses and notifications via client apps like Pinafore. In order to use this functionality, you need to ensure that whatever proxy you've configured GoToSocial to run behind allows WebSocket connections through. diff --git a/docs/locales/zh/faq.md b/docs/locales/zh/faq.md index 8383d7462..a49272202 100644 --- a/docs/locales/zh/faq.md +++ b/docs/locales/zh/faq.md @@ -2,7 +2,7 @@ ## 用户界面在哪? -GoToSocial 的大部分内容只是一个裸服务端,用于通过第三方应用程序进行使用。可通过 [Semaphore](https://semaphore.social/) 在浏览器使用,可通过 [Tusky](https://tusky.app/) 在 Android 使用,可通过 [Feditext](https://github.com/feditext/feditext) 在 iOS、iPadOS 和 macOS 使用。这些应用程序兼容性最好。任何由 Mastodon API 提供的实例功能都应该可以工作,除非它们是 GoToSocial 尚不具备的功能。永久链接和个账户页是通过 GoToSocial 直接提供的,设置面板也是如此,但大多数交互都是通过应用程序完成的。 +GoToSocial 的大部分内容只是一个裸服务端,用于通过第三方应用程序进行使用。可通过 [Pinafore](https://pinafore.social/) 在浏览器使用,可通过 [Tusky](https://tusky.app/) 在 Android 使用,可通过 [Feditext](https://github.com/feditext/feditext) 在 iOS、iPadOS 和 macOS 使用。这些应用程序兼容性最好。任何由 Mastodon API 提供的实例功能都应该可以工作,除非它们是 GoToSocial 尚不具备的功能。永久链接和个账户页是通过 GoToSocial 直接提供的,设置面板也是如此,但大多数交互都是通过应用程序完成的。 ## 为什么我的贴文没有显示在我的账户页面上? diff --git a/docs/locales/zh/getting_started/reverse_proxy/websocket.md b/docs/locales/zh/getting_started/reverse_proxy/websocket.md index e1391ec45..f265edf37 100644 --- a/docs/locales/zh/getting_started/reverse_proxy/websocket.md +++ b/docs/locales/zh/getting_started/reverse_proxy/websocket.md @@ -1,6 +1,6 @@ # WebSocket -GoToSocial 使用安全的 [WebSocket 协议](https://en.wikipedia.org/wiki/WebSocket)(即 `wss`)来通过客户端应用程序(如 Semaphore)实现贴文和通知的实时更新。 +GoToSocial 使用安全的 [WebSocket 协议](https://en.wikipedia.org/wiki/WebSocket)(即 `wss`)来通过客户端应用程序(如 Pinafore)实现贴文和通知的实时更新。 为了使用此功能,你需要确保配置 GoToSocial 所在的代理允许 WebSocket 连接通过。 diff --git a/docs/locales/zh/repo/CONTRIBUTING.md b/docs/locales/zh/repo/CONTRIBUTING.md index 4d05b180e..4da713ad3 100644 --- a/docs/locales/zh/repo/CONTRIBUTING.md +++ b/docs/locales/zh/repo/CONTRIBUTING.md @@ -24,7 +24,7 @@ - [浏览代码结构](#浏览代码结构) - [风格/代码检查/格式化](#风格代码检查格式化) - [测试](#测试) - - [独立测试环境与 Semaphore](#独立测试环境与-semaphore) + - [独立测试环境与 Pinafore](#独立测试环境与-pinafore) - [运行自动化测试](#运行自动化测试) - [SQLite](#sqlite) - [Postgres](#postgres) @@ -400,9 +400,9 @@ GoToSocial 提供了一个 [testrig](https://github.com/superseriousbusiness/got 没有模拟的一个东西是数据库接口,因为使用内存中的 SQLite 数据库比模拟所有东西要简单得多。 -#### 独立测试环境与 Semaphore +#### 独立测试环境与 Pinafore -你可以启动一个在本地主机运行的独立测试服务器 testrig,可以通过 [Semaphore](https://github.com/NickColley/semaphore/) 连接。 +你可以启动一个在本地主机运行的独立测试服务器 testrig,可以通过 [Pinafore](https://github.com/NickColley/pinafore/) 连接。 要做到这一点,首先用 `DEBUG=1 ./scripts/build.sh` 构建 gotosocial 二进制文件。 @@ -412,14 +412,14 @@ GoToSocial 提供了一个 [testrig](https://github.com/superseriousbusiness/got DEBUG=1 ./gotosocial testrig start ``` -要在本地开发模式下运行 Semaphore,首先克隆 [Semaphore](https://github.com/NickColley/semaphore/) 存储库,然后在克隆的目录中运行以下命令: +要在本地开发模式下运行 Pinafore,首先克隆 [Pinafore](https://github.com/nolanlawson/pinafore/) 存储库,然后在克隆的目录中运行以下命令: ```bash yarn # 安装依赖 yarn run dev ``` -Semaphore 实例将在 `localhost:4002` 上启动。 +Pinafore 实例将在 `localhost:4002` 上启动。 要连接到 testrig,导航至 `http://localhost:4002`,并将在实例域名栏输入 `localhost:8080`。 diff --git a/docs/locales/zh/repo/README.md b/docs/locales/zh/repo/README.md index 82761a4b5..24b5591fc 100644 --- a/docs/locales/zh/repo/README.md +++ b/docs/locales/zh/repo/README.md @@ -113,7 +113,7 @@ Mastodon API 已成为客户端与联邦宇宙服务端通信的事实标准, 大多数实现 Mastodon API 的应用程序都应该可以使用 GoToSocial,但以下这些优秀的应用程序已经过测试,可与 GoToSocial 可靠地配合使用: * [Tusky](https://tusky.app/) 适用于 Android -* [Semaphore](https://semaphore.social/) 适用于浏览器 +* [Pinafore](https://pinafore.social/) 适用于浏览器 * [Feditext](https://github.com/feditext/feditext) (beta) 适用于 iOS, iPadOS 和 macOS 如果你之前通过第三方应用来使用 Mastodon,使用 GoToSocial 将是轻而易举的。 diff --git a/web/template/index_apps.tmpl b/web/template/index_apps.tmpl index 19a474692..480a12f0b 100644 --- a/web/template/index_apps.tmpl +++ b/web/template/index_apps.tmpl @@ -29,27 +29,27 @@
  • -

    Semaphore is a web client designed for speed and simplicity.

    +

    Pinafore is a web client designed for speed and simplicity.

    - Use Semaphore + Use Pinafore
  • @@ -115,4 +115,4 @@
-{{- end }} \ No newline at end of file +{{- end }} From 44b7bc71b660a250ae7800d2655a88085cec6860 Mon Sep 17 00:00:00 2001 From: CDN Date: Mon, 2 Dec 2024 17:48:53 +0800 Subject: [PATCH 19/26] [docs/zh] Update zh docs: synced to da4db81bcf1a66d0de559015e061e602d8f2fcb8 (#3589) --- docs/locales/zh/api/swagger.yaml | 415 ++++++++++++++++++++++- docs/locales/zh/configuration/storage.md | 4 + docs/locales/zh/user_guide/posts.md | 3 + 3 files changed, 420 insertions(+), 2 deletions(-) diff --git a/docs/locales/zh/api/swagger.yaml b/docs/locales/zh/api/swagger.yaml index 7751c47e3..070a9448c 100644 --- a/docs/locales/zh/api/swagger.yaml +++ b/docs/locales/zh/api/swagger.yaml @@ -4980,7 +4980,7 @@ paths: - description: 此表情的代码,将被实例居民用于选定对应表情。此代码在实例上必须是唯一的。 in: formData name: shortcode - pattern: \w{2,30} + pattern: \w{1,30} required: true type: string - description: 此表情的 png 或 gif 图像。动画 png 也可以!为了确保与其他 fedi 实现的兼容性,默认情况下表情大小限制为 50kb。 @@ -5130,7 +5130,7 @@ paths: - description: 用于表情的代码,将被实例居民用于选定表情。此代码在实例上必须是唯一的。仅适用于 `copy` 操作类型。 in: formData name: shortcode - pattern: \w{2,30} + pattern: \w{1,30} type: string - description: 此表情的新 png 或 gif 图像。动画 png 也可以!为了确保与其他 fedi 实现的兼容性,默认情况下表情大小限制为 50kb。仅适用于 **本站** 表情。 in: formData @@ -5639,6 +5639,417 @@ paths: summary: 吊销实例密钥 tags: - admin + /api/v1/admin/domain_permission_drafts: + get: + description: |- + 该端点将返回按时间倒序排序(最新优先),并带有连续 ID 的域名权限草案(ID 值越大,草稿越新)。可以通过返回的 Link 标头解析下一页与上一页查询。 + + 示例: + ``` + ; rel="next", ; rel="prev" + ```` + operationId: domainPermissionDraftsGet + parameters: + - description: 仅显示给定订阅 ID 创建的草案。 + in: query + name: subscription_id + type: string + - description: 仅显示针对特定域名的草案。 + in: query + name: domain + type: string + - description: 筛选“屏蔽”与“放行”类型的草案。 + in: query + name: permission_type + type: string + - description: 仅返回早于给定 max ID 的条目(用于向下分页)。具有对应 ID 的条目不会包含在响应中。 + in: query + name: max_id + type: string + - description: 仅返回晚于给定 since ID 的条目(用于向上分页)。具有对应 ID 的条目不会包含在响应中。 + in: query + name: since_id + type: string + - description: 仅返回相邻且晚于给定 min ID 的条目(用于向上分页)。具有对应 ID 的条目不会包含在响应中。 + in: query + name: min_id + type: string + - default: 20 + description: 要返回的条目数量。 + in: query + maximum: 100 + minimum: 1 + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: 域名权限草案。 + headers: + Link: + description: 下一查询与上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/domainPermission' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 查看域名权限草案。 + tags: + - admin + post: + consumes: + - multipart/form-data + - application/json + operationId: domainPermissionDraftCreate + parameters: + - description: 该草案要针对的域名。 + in: formData + name: domain + type: string + - description: 草案类型为“放行”或“屏蔽”。 + in: formData + name: permission_type + type: string + - description: 对外公开展示时混淆具体域名。例如:`example.org` 将变为类似 `ex***e.org` 的字符串。 + in: formData + name: obfuscate + type: boolean + - description: 对此域名权限的公开评注。若您选择分享此权限设定,此评注将与权限条目一起显示。 + in: formData + name: public_comment + type: string + - description: 对此域名权限的私人评注。仅显示给其他管理员,因此这是一个可用于记录为什么某个域名最终被添加此权限设定的有用的内部手段。 + in: formData + name: private_comment + type: string + produces: + - application/json + responses: + "200": + description: 新创建的域名权限草案。 + schema: + $ref: '#/definitions/domainPermission' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止访问 + "406": + description: not acceptable 不可接受 + "409": + description: conflict 冲突 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 使用给定参数创建一条域名权限草案。 + tags: + - admin + /api/v1/admin/domain_permission_drafts/{id}: + get: + operationId: domainPermissionDraftGet + parameters: + - description: 域名权限草案的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 域名权限草案。 + schema: + $ref: '#/definitions/domainPermission' + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止访问 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 获取具有给定 ID 的域名权限草案。 + tags: + - admin + /api/v1/admin/domain_permission_drafts/{id}/accept: + post: + consumes: + - multipart/form-data + - application/json + operationId: domainPermissionDraftAccept + parameters: + - description: 域名权限草案的 ID。 + in: path + name: id + required: true + type: string + - default: false + description: 若已经存在一条具有相同域名与权限设定类型的草案,使用新草案的字段覆盖现有权限设定。 + in: formData + name: overwrite + type: boolean + produces: + - application/json + responses: + "200": + description: 新创建的域名权限草案。 + schema: + $ref: '#/definitions/domainPermission' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止访问 + "406": + description: not acceptable 不可接受 + "409": + description: conflict 冲突 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 接受一条域名权限草案,将其转换为会得到强制执行的域名权限。 + tags: + - admin + /api/v1/admin/domain_permission_drafts/{id}/remove: + post: + consumes: + - multipart/form-data + - application/json + operationId: domainPermissionDraftRemove + parameters: + - description: 域名权限草案的 ID。 + in: path + name: id + required: true + type: string + - default: false + description: 删除此域名权限草案时,为目标域名创建一个域名排除条目,以确保之后不会为此域名创建草案。 + in: formData + name: exclude_target + type: boolean + produces: + - application/json + responses: + "200": + description: 被移除的域名权限草案。 + schema: + $ref: '#/definitions/domainPermission' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止访问 + "406": + description: not acceptable 不可接受 + "409": + description: conflict 冲突 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 移除一条域名权限草案,可选择忽略所有之后的针对给定域名的草案。 + tags: + - admin + /api/v1/admin/domain_permission_excludes: + get: + description: |- + 返回按时间倒序排序(新创建的条目优先),并带有连续 ID 的域名权限排除条目(ID 值越大,排除条目越新)。可以通过返回的 Link 标头解析下一页与上一页查询。 + 示例: + ``` + ; rel="next", ; rel="prev" + ``` + operationId: domainPermissionExcludesGet + parameters: + - description: 仅返回针对给定域名的排除条目。 + in: query + name: domain + type: string + - description: 仅返回比给定 max ID 新的条目(用于向下分页)。具有对应 ID 的条目不会包含在响应中。 + in: query + name: max_id + type: string + - description: 仅返回比给定 since ID 新的条目(用于向上分页)。具有对应 ID 的条目不会包含在响应中。 + in: query + name: since_id + type: string + - description: 仅返回比给定 min ID 相邻且更新的条目(用于向上分页)。具有对应 ID 的条目不会包含在响应中。 + in: query + name: min_id + type: string + - default: 20 + description: 要返回的条目数量。 + in: query + maximum: 100 + minimum: 1 + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: 域名权限排除条目。 + headers: + Link: + description: 下一查询与上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/domainPermission' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止访问 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 查看域名权限排除条目。 + tags: + - admin + post: + consumes: + - multipart/form-data + - application/json + description: |- + 被排除的域名(及其子域名)在导入或订阅域名权限列表时不会被自动屏蔽或放行。 + 您仍然可以为被排除的域名手动创建域名屏蔽条目或域名放行条目,被排除之后,与该域名关联的任何的已有或新创建的域名屏蔽条目或域名放行条目都将被继续执行。 + operationId: domainPermissionExcludeCreate + parameters: + - description: 要创建权限排除的域名。 + in: formData + name: domain + type: string + - description: 对该域名排除条目的私密评论。 + in: formData + name: private_comment + type: string + produces: + - application/json + responses: + "200": + description: 新创建的域名排除条目。 + schema: + $ref: '#/definitions/domainPermission' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止访问 + "406": + description: not acceptable 不可接受 + "409": + description: conflict 冲突 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 使用给定参数创建一个域名权限排除条目。 + tags: + - admin + /api/v1/admin/domain_permission_excludes/{id}: + delete: + operationId: domainPermissionExcludeDelete + parameters: + - description: 该域名权限排除条目的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 被移除的域名权限排除条目。 + schema: + $ref: '#/definitions/domainPermission' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止访问 + "406": + description: not acceptable 不可接受 + "409": + description: conflict 冲突 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 移除一个域名权限排除条目。 + tags: + - admin + get: + operationId: domainPermissionExcludeGet + parameters: + - description: 域名权限排除条目的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 域名权限排除条目。 + schema: + $ref: '#/definitions/domainPermission' + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止访问 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 获取具有给定 ID 的域名权限排除。 + tags: + - admin /api/v1/admin/email/test: post: consumes: diff --git a/docs/locales/zh/configuration/storage.md b/docs/locales/zh/configuration/storage.md index 24bb40d45..c2f1c467f 100644 --- a/docs/locales/zh/configuration/storage.md +++ b/docs/locales/zh/configuration/storage.md @@ -1,5 +1,9 @@ # 存储 +When configuring an object storage backend, the `storage-s3-endpoint` **must not** include the bucket name. That's what `s3-bucket-name` is for. Using subfolders in a bucket isn't currently supported. + +配置对象存储后端时,`storage-s3-endpoint` **不得** 包含存储桶名称。`s3-bucket-name`负责配置存储桶名称。目前不支持使用特定存储桶的子目录作为存储后端。 + ## 设置 ```yaml diff --git a/docs/locales/zh/user_guide/posts.md b/docs/locales/zh/user_guide/posts.md index 8c41dcd77..911eb2beb 100644 --- a/docs/locales/zh/user_guide/posts.md +++ b/docs/locales/zh/user_guide/posts.md @@ -36,6 +36,9 @@ GoToSocial 为贴文提供 Mastodon 风格的隐私设置。从最私密到最 ### 互关可见 +!!! warning + 目前暂时无法将帖文可见性设为“互关可见”。 + `互关可见` 的贴文只会显示给贴文作者和与作者*互相关注*的人。换句话说,只有在满足两个条件时,其他人才能看到: 1. 其他账户关注贴文作者。 From dce85a2b7a73814bb936d1b3143e46a7a99cb4ed Mon Sep 17 00:00:00 2001 From: CDN Date: Mon, 2 Dec 2024 17:50:32 +0800 Subject: [PATCH 20/26] [feature/themes] Add auto-switching themes for blurple/brutalist/solarized (#3588) --- web/assets/themes/blurple-auto.css | 10 ++++++++++ web/assets/themes/brutalist-auto.css | 10 ++++++++++ web/assets/themes/solarized-auto.css | 10 ++++++++++ 3 files changed, 30 insertions(+) create mode 100644 web/assets/themes/blurple-auto.css create mode 100644 web/assets/themes/brutalist-auto.css create mode 100644 web/assets/themes/solarized-auto.css diff --git a/web/assets/themes/blurple-auto.css b/web/assets/themes/blurple-auto.css new file mode 100644 index 000000000..817a07248 --- /dev/null +++ b/web/assets/themes/blurple-auto.css @@ -0,0 +1,10 @@ +/* + theme-title: Blurple (auto) + theme-description: Official blurple theme that adapts to system preferences +*/ + +/* Default to dark theme */ +@import url("blurple-dark.css"); + +@import url("blurple-light.css") screen and (prefers-color-scheme: light); +@import url("blurple-dark.css") screen and (prefers-color-scheme: dark); diff --git a/web/assets/themes/brutalist-auto.css b/web/assets/themes/brutalist-auto.css new file mode 100644 index 000000000..080360c87 --- /dev/null +++ b/web/assets/themes/brutalist-auto.css @@ -0,0 +1,10 @@ +/* + theme-title: Brutalist (auto) + theme-description: Official (Pseudo-)monochrome brutality theme that adapts to system preferences +*/ + +/* Default to brutalist theme */ +@import url("brutalist.css"); + +@import url("brutalist.css") screen and (prefers-color-scheme: light); +@import url("brutalist-dark.css") screen and (prefers-color-scheme: dark); diff --git a/web/assets/themes/solarized-auto.css b/web/assets/themes/solarized-auto.css new file mode 100644 index 000000000..8324ef5f7 --- /dev/null +++ b/web/assets/themes/solarized-auto.css @@ -0,0 +1,10 @@ +/* + theme-title: Solarized (auto) + theme-description: Solarized theme that adapts to system preferences +*/ + +/* Default to dark theme */ +@import url("solarized-dark.css"); + +@import url("solarized-light.css") screen and (prefers-color-scheme: light); +@import url("solarized-dark.css") screen and (prefers-color-scheme: dark); From 9609c4550d0cf6010ab88357fb5636e42ad22ba7 Mon Sep 17 00:00:00 2001 From: Victor Dyotte Date: Mon, 2 Dec 2024 06:24:48 -0500 Subject: [PATCH 21/26] [feature] Add global instance CSS customization setting (#3352) Allow instance admins to add custom CSS that will affect every page of their instance. This is done with a new CustomCSS instance setting that works pretty much exactly like the Users CustomCSS property. This custom CSS is then requested for every page load. User styles/themes take precedence over this CSS. Co-authored-by: tobi --- docs/admin/settings.md | 8 ++++ docs/api/swagger.yaml | 8 ++++ .../public/admin-settings-instance.png | Bin 205194 -> 234878 bytes internal/api/client/instance/instancepatch.go | 1 + internal/api/model/instance.go | 2 + internal/api/model/instancev1.go | 2 + internal/api/model/instancev2.go | 2 + .../20240924222938_add_instance_custom_css.go | 44 ++++++++++++++++++ internal/gtsmodel/instance.go | 1 + internal/processing/instance.go | 11 +++++ internal/typeutils/internaltofrontend.go | 2 + internal/validate/formvalidation.go | 10 ++++ internal/web/about.go | 2 +- internal/web/confirmemail.go | 5 +- internal/web/customcss.go | 19 ++++++++ internal/web/domain-blocklist.go | 2 +- internal/web/index.go | 2 +- internal/web/profile.go | 3 +- internal/web/settings-panel.go | 1 + internal/web/signup.go | 7 +-- internal/web/tag.go | 2 +- internal/web/thread.go | 3 +- internal/web/web.go | 30 ++++++------ web/source/settings/lib/types/instance.ts | 1 + .../views/admin/instance/settings.tsx | 17 ++++++- 25 files changed, 158 insertions(+), 27 deletions(-) create mode 100644 internal/db/bundb/migrations/20240924222938_add_instance_custom_css.go diff --git a/docs/admin/settings.md b/docs/admin/settings.md index 0efb5bf45..170d07e6a 100644 --- a/docs/admin/settings.md +++ b/docs/admin/settings.md @@ -167,3 +167,11 @@ Links to the set contact account and/or email address will appear on the footer The selected **contact user** must be an active (not suspended) admin and/or moderator on the instance. If you're on a single-user instance and you give admin privileges to your main account, you can just fill in your own username here; you don't need to make a separate admin account just for this. + +### Instance Custom CSS + +custom CSS allows you to further customize the way your instance looks when visited through a browser. + +This custom CSS will be applied to all pages of your instance. Users themes and CSS still take precedence over this customization. + +See the [Custom CSS](./custom_css.md) page for some tips on writing custom CSS for your instance. diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml index 5c0c2ae3d..a3a79e2fb 100644 --- a/docs/api/swagger.yaml +++ b/docs/api/swagger.yaml @@ -1570,6 +1570,10 @@ definitions: $ref: '#/definitions/instanceV1Configuration' contact_account: $ref: '#/definitions/account' + custom_css: + description: Custom CSS for the instance. + type: string + x-go-name: CustomCSS debug: description: Whether or not instance is running in DEBUG mode. Omitted if false. type: boolean @@ -1750,6 +1754,10 @@ definitions: $ref: '#/definitions/instanceV2Configuration' contact: $ref: '#/definitions/instanceV2Contact' + custom_css: + description: Instance Custom Css + type: string + x-go-name: CustomCSS debug: description: Whether or not instance is running in DEBUG mode. Omitted if false. type: boolean diff --git a/docs/overrides/public/admin-settings-instance.png b/docs/overrides/public/admin-settings-instance.png index 181a35a7c2166b1a0fd4ee4f215082380922f664..1203e8a416a8c5017c4d95571243e73aac58aa4b 100644 GIT binary patch literal 234878 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ദGrs&K0)B!LT;eZ!a5! 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%0AQPF+<8d?j+ledQX|Ho6 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

)T30&&bM@`BAuqha6p!X=+ygs9Yq{4yNd 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%0Df`FWr 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 literal 205194 zcmdSARaBfy6E;dh0t9!L;32p>1PkuL-Q8UV3BiI)fZ#F-n&3XTyA5uG4=}j9p0#&& zzW={I7iX>0SJP|Wsj04dx~i-Cd85@-WHC@lQ4tUjFy!T=H4qS9-60^nT0ucVKtNEF zRft7EKzQXLDgOxt1qCG##WD&30fEF*M%Po*#oE)y%-ssX#@WTmiq*ry-O9?@!`8*~ z7_nUp0Re&jpNFK}t;{^_T%6y2vU9RRuylFLCGghL%Hk~-2N(BSP97mHZXqr~?z2d7 z1cbK;^3oEYe6tRgeFC*EANx<|uCoGMuIQ-Ejxo4i$$ZN4`XN_Wu5WPtE2+uepe%28 zLTJCf?5<`;Ux&cK=pCjsc}U2?D-`O0<0ZGT+ebGS$q)4}@K+)Iw5&^k5W&>|_eRd+ zz_wK&iI{n9WYpggSfLl-|B~kKI6Gw8!Nd7)ETFyn|NrF1`|3kjCl!h*RE%ZVb302~ zns1|tG9Hhb9Cdx&Ye>*(m_alTO`SpdU{a)TX^Vz6( zj>8OWVwe`6u;0-%=U9=j3#}~&!(tn2coAy;ze}5z< zCZ_Mngj17?)yw@yt{OHRsJGIVK#snzZO@hGT0Qkv%$t738uUlKK=%21;6*GOR5U0OPxvu}g(hGT>_?@bpm5Za z{-0+L^o7qn4lt1c_AS(ZzSe+Y{Bi89*g^uxq7F&tKUJkI*(--<*BKZX+%Kv$7ks*& zE)HfgJHYdr4yq{OlBMj1{VnWbutR>6qQ*u)RaN=_{Vv8^ejL2oBcBz}$u6&~3?(R9 zWqlcn@Q93_{~QuZF1qEL^RS;6DCRSklaulnas$?0cazJH3@YY#GjqWeQf%sTiPablz=RlpX&E3nb$=EP{GfWcPW|Ma{K-JLFIZeA=@^b&=1!_U=q4ZGkmDcGoy zoWva8l4imIvY3qYoH+d+{HTvY0fNWZnbdX1*!~Q?X4jNT$Clko5o5oD@c)yRunZR_ zyKr*;?ZlIxCq-}KxqFmx+51Y0dIPqxh*_9l`uVjrNzxTXSa|2TVQkf6>TQs?pXVg% zCZ^%6*fUvBI)7Geac`jBhWFL$zs9lTzfpM&Amg2mI&3A1IX{`lK55K}+4pZKkAyeT z_4J$RY2U-+ID2|uzbh#<9~=_F-PmvyNgb)>spjIUJXy^2I`BRP2y9Mkrn`5@-FZQ% zg=bV9yk3|DU!VpDoMC9!>P+44a^41(i#+T0U8+!P)Te<#K{0Z}4Dj>g{tA<}%->8g zzH1MWZ=N@CRm3d?8r{mivpuH~Q3ZVgXG`5~6%={wL^;z%Fza#NzovW`R%Ed|L5Ojl za^6T_mW5#j_hmC0b?@_1UVTIJ+m1FFFIlr=y9J0Pu|K^QCxxb>mxOjztgQb=ejd8t z|J;1L9sM8r22uiTKDS+M)mA;*#Wa)m|K!EbZ7fLc+qTx%*MAey^@Rq9yub5YL_bWz zk-2HvwEtwR-buUMC=%{(DiT-^MC^+%ly&A?pQj9J&Q2Pr%!`M2h>hzTDXH6WiTYz& zPcqGMR!8>h&9zH3I>_a?3SO`H(})6)M2Nsp;-It4pFA$q2Hb5pSzfEH`BrorUql0+ zc>Lo8b9S#|VV>^0DfrX}3pcQ4&b|5J+dRzy5}&aGKeDO1yPSbTEc&GMJ>0LlH!ssK z#MgM7_m0$puJQsYjxL1h{(@Q949E|&t?1f6N!$0Ut*_TGH16CWC`oHJ#R0MD_B_xJ z>t%p59=yWw2~*V!wN>Q(Ue{s79TEU2AZVrrgh8OVRX8Ity1J&$bKFPDW-Z^>>`%9Z z<>L3R;&ZMv$3$SB<$hM8Y=lfOESvUEhzwy;DuOALVqtSFX+>I&h9K=M^1;_ANOf0{*f8 zD7^zxV84DpV!RwHP9GN=D=;)iu#V8DK*PHbQbE-n*<*HMn{h4zVJq%+%hbESifL)@ z6}=GAhmWppZN)V>Q=o}wsFtDmN^G~@;VP0DE=BYPid1m&#fQPx{&LdQQSxZ|F>g}Qt~IEpW5acjkbwI~LSO~8;R=~v8?^!lH~qx0zy zeW$R}Z~(6pcKaL|xpF zVaNSC+Al3k9`kpT_(&EZ0mveK3@}6aD2PxanONAYdNb;H_`qv5P7h)M-MY3F58Vg8 zEt@Y=s-gsN2-!^JwUjBOkgsT21^sHy7p)FDA0DvU;ju4*S~9|09&SoT3|2!(Jovy5 zj&x5~p`$#!e%M&Q(|2z|P(tKs=>3Le(&B3x?b}_Tzvp`0t~zjS{s#R8>Sn<1QDJgm zS(!x2cA541Suzi}cH{b%4;32ANhaBrJ?&j<#lm7yRo?KOortxdrP#XW9BQ?3$MGYT zDok-h&kzXtH2xni9PfqMmLVtI5F!UsD<+D!qG9$IsTQoBFSRQFcZ# zy{qU0)}8_H!ZQpEZvAiHqbev=v7&*TU=y=H!Z4Eq$*vZ|c3_z$E&C0n*JNluqEbBE zJg;5`74Pq*U0>q%#SwpW_SR-ugSwU((KMtybfF+)og{c1)GZ}f#}<~;E(H7X;b1Ki zZTj;D-gGr*i3Yu2)?jp9NOExvDv|A&Qv$>w4;nQ8O`1^;*k1O2x&Te0eci<>d1(?O+zipkAnJxd(p6fIeA%%c!-}&am zyq~_We}Y~#dFepOf75b;l`*Dt#4`~WR>fKZaO^lP`rR1*S}|2qOS5^R#YayIRP)v5 z*_FS7ouyE)oF5N!8T4XZwGp1nd~0KzFV(X%akB9Eus2I|Q~D#m5{^@x)?dSK*mG@mqAp-VK4z6aIJ>s+y^ZuQLjtoS|llf@)$_9yiyb+Eo(X( ziCumH9WD`3N4=v6gq9dpb9G+YPKLL)fMA-B=Y^?WgxWhv?T-E9b&5g&|4lXRgzk-G2VE1x_h8GC;4q{RkgfBIAT( zUgk*3qSetah*QSyuhnKv8DMHU#a_R>!n|vbbbAJZsc9@VO9tb zX+&{z;T=CyDBrH#pk;oMqd+T}(G{6nmhGi!sjv%_Fu0ru;s!G>S69Cg#$jPuWPeYSX_ip=%o__zQ+_NZOTS(r;;Hl2)jf)c`eG5x zH`v42k243C#B3S2xWa>|=T)lyImP5_dhJmH>YX%oH9l%8qNO<1Ap;_8b#0)0R>@l5 zb#!1r-W6B!$fwrks@1f0x}IK~cxhKrEXbuRIZ?kCXP+rq80SO;X$~C6&5#DC=&{>v z`AYOEmUF(!Ub6qxmAs<0Q}!J$H7|Wa^nE-XBXg1*-DfMkFBFkl4rtH{cwK)CO|@UN zC6F|-;*7V=--*!6D^B_|E16hVCRBNn#`#2s-1EsUzyI*+&42bjm=UVWh19c>W@PA^ z=#Q-hwOS;*6#^h)kn?x%*rJb0Y@pKQkr&gJeR0IGu!iXw{yJpecs#VDkS|{ZG&FHh zUOh~pk%aB4)xl*{F)2vxczEkHR_7%-h&$7Ef&wS@MA6zI47`HTZnVhU9#^T-hR7x? zUrLM1@*Gkx5PcJo(8k-5J?1|6TCnmj8z+3F~({W0!l zWPxUw7#3F8&mRq6+C-Zy^vFK*L#u5a?SAGqG1Na(Hsr`wI5*bTe|-0ziGlblcCo^mF{-!3=+$kdv00_D^XJs0?_R#Uz+2!8z^5&5%a#y4&14Ax zS3XcsauB*nv2Xo2Hxhe-#!NFE{8#_p&I)<$vKiMFS2WefR~HvweSpw5m4b^4Q&MK% zrdX{vvAuC~yI~c7z0drs%ptdq_Y16{!I@{1J0;v|L7E)+wODAjtl}+xylF3~Z%2f4 zTMpcuL`uqn*#T|%cZo|Et$wdfZ0C80mr)l^lw_L(C;8 zvRPMetDK%_swrMB%kQUzva`J~`2mw%;%ByulX<8d7Q9(XlDj-k-~IY0L>?#$@$NX3 zWO-0Te$Gv_-J&QIeaBo;-06|0d$``#@z>9$>R36tr4@-eVvdx~dW=prV(1wT z6;b4V(|o1n==>-*m&<#aB$l4sg1<^YM6h^ox*JGYLRUv$T1Sy&9K zxjm%)FKdB^F*-W$ z&GVziF{a@I!Zlg4`@~VpjF#2+WPDt7K22kNIV~$DF%U3i_HCRpt8V6}OA^F7FXgJo z`yctXtaZG}wY5ANhH0vb9u~Cj27OW6kcI}`dx)-;sgV?IOiD#=ZWB^K(g$^E zA-#9N7RF;!r$bDyi~mwVg0iRoIMMPV8|f@Ta>~^FbnGg;sHHf=RR9;t4(YCE3G|Mv z^QXApK4rRjPAQ)YL@9?3GieGh?T7OtZFqz5EuphHG-OdRxj z^N|R9Ns+}=f_tZCz#s=h6U5X)liL9`hR^d#Yo4*%NrgLM0Q14%%~sO$rWT8#epxHW ze`m|K0+qui=vasqdfezJeyYy5tnYH_c6yH7LG0S{HhhZ?5YrA{Y=?}5#Eeh~oR%Eb z3@WBE#g!Xy+B+!#tq-;RWkXT|5N@^A#K@e!V4I%P#(NVfCnrZk&G}}|%L3MNW!)NK zTS=U$&NI$EG4PWY9}RyYKlE3Wqw$W#_Od?`vj&Xc`I8fd`APWPlyGbJF=3a3?&I`K z;ANep*q;=dFjlGfxvjoqgjmWB;T-W)M?k}Ru2>6EoN*_fahA$gkPya+w02l)ze}5V6*wyI~t=-zk6^Kyu z^IKDda>U~4t8C0*tO~13O`R%Jw}Ss^tWxdp{w@m1LJV41T%Xd^_tCw&`J0Ivu~E50 zha1Jqpv;C4(5xx(+~GKV_a#S)@^ud4z{rt`p`l2M6|9D}x~VWRCu5|zMJw>8GtxIl z_AT3oxdTV{CTn$_RV9EF_vxF#iz9LLem6NexgIyej_&Rlty7t6TqUKbytYA46f)`! zp?vznaqc3AT2zku9vyX%9I`QDTyk9+fbm<>>B~5+v3lSO6r#%Q-4fl(Uy^^DPjZZl zRuWVQeQVN&s44j4a8(;8k;AqMyqmH$joo2Ms_$Bq_gC?_*0%nY$A zg4JMzhJZFVeJ3?=-9%GMOIi93h2tY4khr+GuON}PL(whOSO1-7< zU*tZ5c@;si2Ni^q)5i7i^>A&qlQ9-jb1XkfY&Oe7ZXM2Gv&R49hI+*?G06GJ0-uc4 z7W%=tXp^hrCCSMz@1S-rWrkp92Oj#xW9m*<}o z=>#oheoxIUn*4oJn!LZWo+ZQJGt00EQCT7F;`hA6v^X)-qzDSKm;7^H(C4MA+ba*p zb;I&~a}p_scRkfDb%7Swt@BR+W#!Q?T?tnmXZN)6-On6JBy+L-?T4KPrreuL?dKq9 z(YTjRn?fRRyF8tp6EIS4B!d|;lRZV1J($$KLk3ts9&`9xeIA%Mi>!0>t<|vDlkM5r zIu`m|I|sIMu{Yt*)$2C;Q#=O16Em}A1jwCTzE|13UQYR3KPuYR2wNPRt7n-fI;pa> zqogO8xp8V1wdCphQ0mUH(IgNr)p6|d9B5OOc<+7!RNZQjy0X%(P{J+az#u5mJ2^FI zIqS+~DPI}uLu!td-AdGJ(M(qjYd)MHJ5Q>ERAu>30@G?$gWWX{8(8b`S?^=7je|pV ztSj=ikgL79*Vo8=9as4Uj-Osj*K_ox3P;zlivwX-hpN^;!m9~9e7CA1ea*~R*+S$k zE=Y9Twg!>M#g_}*ZzDzSJHlF;TGSOy{|nvu%SXE$I0oznYM&&pvkrC=^&#EWEc*IV zflo7B83y{N3xv**9c49Lb#YQ+z=jae#-suv+pq)HOK9ww7kf#qH{TH z)Nd8M+%`z-dT!XwvQrW~HK)93;)kzz3nJ4a>RM7{X zWyVywmspbiug+gkX|@{8g@;1mdHL0eW{CIy_}ENd(Cvk|7)V*(kJn=xFxq(^N3(X0 z7kwBr$9VuvFQ{2z`1y0+ujlsh)9m!wU`sC#FW4M<^$`}w*-#z>6S~+-QS3NoB#-u7 zoHR=EN6=aEMvVg+b{)eDxVI)V{r1+{@`)o;zBY;kJ$e~d9r01#T{4ZdUkIWa$`NzL z4YOzU-AJ_VWZ)M%?lwPMmPe*Fijj-joN=hMY$%M0SaR&Dki@J6W;9CkeF~tb8Z~04 z8f_sHH(&E>U#pa0swEtSCEp*dXMfjh*z017J*1R{Wh{sQ;2>WsD>PH?gN@*m@in;M z)*i{H%%iNQ6Fl_W<6}LcD;gDXUk;-dd+nf`;D_BdlFho8^B1C9*5zamob8P_e)dFp2u`GV>;wr!x64!6`l_h9{e#*lI%ZnEc2o4pe~!DsMs!SBvkA8!aYP7*Mv-!h zD78&`mj1b)0y${NWz7HEYh!+YAFP@+DsJ6RxJY}q7j=52qNmvY`Fi6^;bFk+58eAy zej6|mnh%3`SR4;O+Fj4q7e<~`1%(Y=nf~j^1QaZ5Ow183hIgBN9eTerl_wm(kBupd zBAfejWzVa3L&`+wKLmtw1BA z?b!p1_hV*zZko4SiWnCo%AfmW9?rvgl|<}KHu!xan?N0`J|-VltyEoS^u)Ig9G?4z zLWw+%mHVey{c-w?1Np%!r=0sMyIBE81xq&r=YdUZ)rSFf3+(ck{aI~PuT_ouA*!Nb zyQ<|DH{tDD@NNKADac) zoHJt9{y-GYCZO_)kvtm7Gr?-)sW$ylMKEk4o$tmY-E(nblO-r~fu3#tkz{h!>~_wN zEamCrnO-f=$Pb(_Z&eNU|1hwM*38OyB7z0_x9Eo~k^66LkJl+OjFw5v;+OX$p3M69a!fit7x+!j@SA(-RyR+G zga)ejr}OZ1QxDMhd{YndlsC;eBMCt$TU;hVI6|*6y-HPgTN= z=%aU?SzL^PNGeIM6XWmo%XCG->yb9knjd0*xS9;eX@Gmgma5tpSigMt+&)s?p=b&j zLv>4UAhaVl#V?6`&u`NoE-Ee3pEqGG?IP&-r^h;CFq=87{0fX-gLily+s{e&CPvt0 z-pr3g5I`Uo7>gtXDA|I8TiM_2nTf&vR2? z+a=6oL8~Y6T^fun^NmKM1j*ze;wPWYg95mpT#DQuI-XC#lVJjvH*A4-MT)-8Du7xV z*Tdo~ZpTC887DqQK9=Au_7)y?eG;9gK93f_5N#~^_Xs_W=F!^+FXfPs)wNgqzsrCB zENF^2t1}3|`J#8R!!B?%QQtnteV?;SXBa?q{Tx8L6(>J$B0h4lI`(7y^@fI=vSWTr z8!RV|&8zP6)t%Kh+qd~~chW(+O1OC{%@K+~vJaA@b1lBlfCrz;l;V_sGva2XCHOU< zkJu5K`}22n@uCnzJc*2(-fKOQ-B%BMirVFU@N0Dq<6dM?Q16c*QIe&0B0HwRH9vv! z=AYfpEH_Y|rI@Fg81GSDBoHJL-t=8-rqXjuANA`HN_aQ%a8HC63aSMh?dFk=k=|Lr z%qBb#K^t?qW|)a3K14j4E|_(u>SAzcjCvULW0+Wyg1!PCMjWMS0^i1~F~M_U2J{1k zJBF^>zg&RNZq(G-AG-A&5w)G!{nI2@bQfyPNE|I$OVIf2w%2E?oQ<6Bxm4NbC1W%t z%}yiSoE?}Yf;Y5pH&_EFdfoj2s z`;AWh#*4OTPuxtMsfW0b_a8t(%XUU}S@BaosHru%yKwX!GN9GnBvfaBjF}I7;In^i zUz5$eB!oIU>!|y*S2w=2v3`xr0p+C99)qXf+qgq{RZk`-Kxciy#@kz9LN&t04dtaT zpqwPgTG2hv>DVgB%I@qb%d<<*&~H!3*?*gPFE(5T>JWfe1sS|fk{8Y*qil}i%)CX0 zYqfW?raol%8tkoAqxD>aMowWKKQ_^fpM6<7mwk4RF_S)2JOa)3aPB#CgZa6V5V0M9 z6#KF=6SA8gF?yu6*XOuQy&ufSfXRxJ$~S3t9L$NWcCl+Y`m1CO<*)GZ#;llapGCy2 zwjI1s5^^VAc!Px7=X&#nvZc}g5S)?vZ=2wN!ux(m91FN{hKjrKa>SrI_Fx44ibN<% z%Kf)Fp4UEqL zbrGHY%DDyt@hhS*dF8A=btaIgrzK16x1#fH8T{|W^MtW>2 zl(iZsj)@L>^5}6=boJG7B`+sYf*G&rS$b!@JV5*XIc zHN+2sf-+_Om_uZ&wTW0}U#_JPsBdI>FPMD`K<5Xe?HlLZ&TGGk9`M_)Il%2zjSRRF+HfMA;1jjGd5>I0 zs~073H6+I56FUiPa3F=u%BKi)g`U#YhGNJ{U+bb9G)MKm#)Zl3DVWOQskx@{V(?z@ z_FK`-&6d9*77Nr|v4XByC-|;y<@Gnfd!UDkv65Obs|s`>@yNiBWVcwZ`yPL()P z8a_&gkv8O~d2bbbYo8bPbMt!ThS;ZCshs!<_lVH9) zV9rn#@=^DX9KHNkZY0g|K%T#2iQRmj@rq!}D|EfO6*mJl?A&JuAM}b`ZuCxg zd=ZDY)f}67V{^D|#)^EC!Xkz2bOLsBvK4Yy3ku3VcWs^f1^1zg4LIyzuLZ|6_Q$JI ziKN5gvqx^$0Rzgw{;xcmnoaQpXyUN!=eFTQdoaLpJBl`q&x-3F^H9dSTKU({#7pLF z?)^)W7I2F<*|5HoKaf<#pJfxR!asC%=>qk275xo^SWIYMWKx55FMrqNha=s1(REu) za_(_z{(JK|9V-}Si2U*0Kp23H0lcYS*g}vW(y`rC^ z8%iOfi-w)Q#}Mdf^P-IpdwV#NZDR~87s`Lb zr~*$f!Fs!|(jY66}a=MA@IWg>noI4?6ahB=Y~swF5`h_o+wDxD5;S z%34kod6Ae56{ZxoulG;y0kv4W*wgENUuZFZy3#H7b|M(_5I9m<HA}m^KBaz=JM{-qQsdpD%yS<&4)Xs6c?*=cJKp* zbMw&l#xUD9`}3ZJMrABL)nl9kWjc<*hBk!p+GsSJ$ao0i9GErC?U*qaJebt}9KsIv zs_^dOgIho?k9{z8jHiL6(za{{y-TUi+dow^5N__mk#8a2h8xzYG|d+94BfYeb?bp4 zkocokol8ESC%!mENPCv7>Y>WlWyzVn+ZM0kOOZwvQl@E08*o*OH+y%AYW+efris_X z@5tx+#JUlUn1k;{I~Mbm#-VReZ08yB33PGv_VcWe0xUZ3F-Ivv1nyK@BUZBozj`*q@Z_kQ zAt=>N_k+Q3qm(RCqb0gqR96gj2rtz==>;_(tNEYk-~pA$HE)~W90Z2jStcqFsp z@106W;hg?pF+^9}^@EF~bvZy|7q1J8>LZM#pU3QBa|* z+oDi=aj?$lG3pmNQNaDwsK0yQaq#-VyT>wu(IpKe?t`5a#Fed%2-*Y(2Nrctnu$La z8bL!J%Y+DN_4$o3anoNBH~!N83#S5z8c{IhJg zp*O;wM=l)E7ukVaMXK2&^AgMQK(L!Z3DcOtNGY5W*6260y8oCGXmqvSFr~Wv>rr;R z!huVyi|0F0rTGf_)7=2Hbi!YA_m#48oUqqY$O|bUKZe!#mm^ORabU z`pvwk_P;P2NUK7(dt{$8dPWW3W+)-8iI&hbQa_8Df7YN)4RSnzd&Cr4M;aJxqs=9l?@X9urv-jha1G_$(F{b zRGQPHrKGIW`4m8Yys)hB%NTafl&?b$?G%SWg3}aKp=*8^o1=lr&j78^3W#Np`$pU5 z9fq+05MJJ#po0p&7_*lXa}~l;1<6&vfc&;Tb$ISistEeH&fknA1C%4}r@uBFUe_b^ z#hfu|A7$oFRZrzf%Tvke8c-sG@p3;`IW#=Be)NwQk4orfbX%NG$R|FL^lNzpV(G#W zjK6jHQPsp!lZym#SQKTAZRYT;X*?|qw={7yVTBpgWC7dX<V<lIX=B{%02GZqs`|!mcp?N|IUyf5Fo)MB4Jbvc9loIBxSH=?MC@DyQo`yP z+hW;LA05TQLxpc)zyS06d$*mvzsay|u7B391Bh!jZsKcK*BGes?i{@Rx>1)wtIHO0SablOy3?;p<^4_K`gzm#qk* zS?rfedpd?*&jd;eyru;c!5@f561hZJ+!-l*zUd0>@Um_kx3|WC6)8Fx?kh`frs!ev z%bU=RtM;*f*%FpMyVI9}DscD>NPBpEu_IK-EgVimKc5PjM-G+QDE49~G?HazT2Ko6&w4=sI zvN6#Jw`Dv|`I+`sEHo#gQKJL{9rUznQ&>Wy4^)Ezr4ZyW1MD}aPX6; z4@6f%nXYL!EPE+4n$f5$8*61SxgUMLGM_*kF%rwsWoe?eDWos9et$brzcuGl<$%2P zZrnljQDIbXu=IL%^2%t~LANWH*J-r~<4yFNs%I3GahDIO2lMB4#vDmliqN((OahM; znD7vFTDPW5>he4OP6DB%A11({< z1{g&)!+VeXlmcG2%G2@AP~B%LYT4cPO~rlEFg}$0x_mlebT0HSuO-2yKvT0W_wz%x z(Aas)RY7%ga|Twfc;eOW1Vyt{_h(kEi_!aynq^L2oCFsXNwgG!aUuWK3ViFv)b)bNX?@AxHSu>)*&nv#MXDeX+%=H}E6DB05HO{wGW* z!af_iOoIq`lmN||;+ z+ps=?MpAPbQ4JOMDGF_3z?#JPrgZJhl-A3eDK4U+=#`DEht%S~(hkF=%vtp-@K_&` zX$gGxK=<9?x&rd2HRN3ET6Ye8w72i}z2OkRZ@&vDeCBp(KH74wYi$=e&-mpK)sq91 zu~s1zL*)~pN|M|wB)f1DXqYm#`L}(2cIdTNvDp3d?YT|+Tzb)xl?WupJKB@{?(#@p z(r_sPwVn0B^iMY5mIY5J-l6bC_ zNYIJ8kSL#b=LNNPQ0rGU$LIDX#CX$@?av&->C;~@fwEZ`p{eZQ(>J5xHqMcIf>Q4G z?(m?zl~U~24vVBg#EJcsLr>{s+bIH$G%&66LZOeniQuA(ds>)^`Dk#6u8kBFBR8+tQ+r@_99`J+E^sfUeL#bjR@=3DaR#KAZ zmq~(ebW>VeLvpvXSNl3BPb{3ubg6Ct7}j1N~4D=fcx^5ME8!50$Nv)Y<4iSido0^g|XdXy+@wsh^`6C+A$` zpvN>vO-}eVVvwTuwo!_^lE6?}68_TUXF~w@GwbT4Xx$*OD}s2AKgZMi?gb0^L9 z{O;0{6}3!)rF-J}EgWP_Uoqv=;L4PK|HhB;+Cr|wRDXPhkYBG}N4- z=y!3#--BR$kFm10kIgL{CAk1W$NgNaG|fhFSxWag1?o&{JO`W03 z9=fON{rHreaVtS#r#@!$p^@C^uEWgUqx?=CtU;+oCc@t`RgP3!IA2vqdJKGD50QnP zRSfR)Cx*JMKA;{C>?{3y*6?mMN@)w1l1O2_$+AKdTh&V4!LK;leG@f+l|=A%4BTit zV*2{!t2PZj=(Kw4ftXu|T;gE64^b7~{is?Ys3&u`q$XlY3r262S%{>#fo5OgX| z|Hvn15hf--olzNS%Vu6)-lJ(ZL?rax&Ge{)Wu?XSrIUG?i3sK~`<&)GpO%)Od;fR; zGF{~00}zucBbc|Z4gK7V3tYw$GI{?^j`6%8F&SrGsptY zZTaGQL32D_XY~V=@*PcejDU)zs;Y!#g}KHxygD*i1^l^}Mc-IX&x^GVgL;-FeY0G=x97O^A#j6iE3leUY803(3 z#D14;56V$LZr`28-r9pJzdteis=B6md!;20xiLQGu!hq*^#Fy9rdlrtj_ZiA;V<0o z$-&KCf@eclof=g;K2O@|1?cI0jdJ7YI83|rNy=@7Ml9o$2kY?>FkA0@|LNb%IC*Pt zUtI67gvk%H6~_(}$L^>H=%&0B$64?)oa=U@`65Lv(zg5*Xx~Tu=OTEO9}11Nh)g}I zKRrPS{~W@%{~w722WMfbb|U8wUo>*W<>)_BZ&cL5Yy6A9FP;5=KN)zg@mXD0|FnZd`8JO3!_He}e=X_CpYP^A&v_G&8DHAW#b5-mqY&m7M}-^bUt_-c_A>vvP2SsQ z`&#zpmv0C(%7NmLB-w>@QID~7?i%jnERT;U)L*buU%a_^L;VF?@}G$$iW><>7Wg{> z1q`X-G%Wz*`q_31tF8)yZ%&(i)9y=KrJ|;BWP17Zo`FJ*WU@ay_49rKyz=zqKq19X z_;mOK_fOPc#UItv!+N8&lvKRc{8mtD|0kQer{M76_1##=3Us6-TbZY%vgV>P0-Ty7 z$Nh=QNJ3MuYV~6Vsx9VZEo_x_2z=^*Q;WvyZb3x!{_C)B$r=s*e>(o7Xp~}y48}$J zvgeogmCic!z@piYrx&OKYW6y66c5#TUsn(N+dX)?UoXtGv`vL7wxKasaNr)7NinHS z{9P*b7wioXJQwM%SdpBr4_knR6;sB+8ts?%-UHuba^c!TVcx$?n`Sd zXr%Pny2%}wEq840jKj*q$1Kvj70~Pol#FTzf|{+*J?0K#vN8VpAvNKAF;5i{|Mgxk ze4MF2HT0uGaq@nRJ#q5d2rcVq%WP+7C9ju-7X=?>roqC__;PL_*)X~P$2VALqOI|zr$*3l1 zHT2<-9Kc>yX5MH7X&1_{b3^OA&&;MRj9f-FnCP8?*SFsi4hY#f4#EOc>aRZ`dw^$Lf2&gegikO(uUmjk z<+xG&A-p@X#Md`7?kw-kJ0XAR3}QSdbEWU0;~)JG3&?+&Yo|*BUoj#HXG13c;QJ5A zcQrOP@GQNBse8mfN=uLQj=k)tB65qJ$M(HT&K7cgM#kHPVG>UM5(N2t_a1VrI{%e$LDf7p>`6c*`SGo#Fz92ovxR>QR1 z4^Piy*yr5jnscDw zf!Cg-q$I+!zclCL2b!xqX3|A>2%{?JxM=DvyNbC>Gn51>9*>~;nxr}9{PFvOQgre{s^^D0|Uk^YT{zsV5ChE&Fk-!th-w((wp9;f6Yr0+7Yqkb;X~%I+jz4Vzibe z(Yps%`i?mgGNeqmBFJ~v%piP3-&NQr?7X@4U(410mRTP|AUy|wYFbOz2vuVx)}MXW+yx{J%dzkZoPX4N}VzO!KB3qV;bM)Tdf-ow+$O(qogqP zb3u&pR=P6IQlOLxZdWUZ{`q|SlT9I@ChODwsUFjaM0SJyw3|7^<*7s;0ZNfft14cGno zuzEJs3@>&AZ}_)O59|P_z(bQ)(Tq>@K|Sxy*D;V7p0Vk#NakXS3nbMHAJRdKP^6sT z4TCE!Dpe?PFfDsu1I)R0eVoMg%tfZXIj3j&imrlfSW1(nmgJg66~iGyy@ZZ`3xPz1 zTABfUbL>X@ru_kZ>Ppuk2~F-tFmP7)fM64DseVWysphEKCtcTZ_7Hq4R8?qPl@(3) zgaY|k&oG@ILK{BmjzO*>eRZT*g#C+cjGl2WJmd$VC%HF2=L=F`TT@oxq3=^`(`x_o zT@$Rl#zG1>AsWRO)_4BBDlJlMQK10o&N|F*qZcags`FvW_1AkLFanK5%lME3V&sa> z((o{z|5}kI)2M=Va3k3$t&scW^wptw^tK7}LKJ-Eiup^pcNfJdYngDlk4EK-|kMMd( z05~uME9^Xf`gSUcJSF{8NMC=KDbI(-tbDl0-pK9 zI1NuXi?69In?D@1`{w4nC|z9#TAs;?A3d;mpW%iD9t4WWVmy9d{3!OL=O@DE({~rN z-NfQJ_4D&^Wox=mHN3Tyw+Tqm#lpum!gZ8!f4#+rrr+MZLgi8}Jch@;rbxQ55xb43 zI=*~?wW;Qte$o0DWv2f2s)Ll$S0}RZ9_&%-1KpGSwDTh4oBhMhS&@;qh0gIF-D?f8 zCm7u&N)67u^z2o$P9W^V#|K4(&uQ{!ljk zBqvquGiu1^pvhA2j%3z*j!-MP8yT{~ywDz@8VXnihE*vk_?e-gd}2M(ad3q^fqr%{ zX*x;cG|{jYp*w9cWoIM9%!!7M{&duDNpu<&ByrH9|KzB^>eic^*T3|-M&Z&|V|iW% zrOL5Awc(`jvexeIIs{3>GzoVz-*6T2yzEx`uglbdKtI9|jgUzUQUK9Mu+!$JA3I!> zO|(4>atiRdQulch5Y*fWFV?4O5S|K_sii5aL#Mm&bnxWiI;hao2y@}(Noz{+;^))I z^YGIH+B=rI8>FtjoPUU*=K$WYit2&8V$bsU@IzA2I=MN0bBNOFUq^N4C(T$xfwNhU%22bC} z*cscGC_0=vQY*xqh7oX@ROd~0{5~aIBV>SJeC#3e&B{NwD3Kwyds}ogMe@ zvv`#|Vse8CK@-iE#hFPVo`u0Aq?q;y(jJN1OD*!>)7yQ}!NNp5_omqK5@nF<<28hU z8pu(?L`)ES#;{iusiHVAIE)Nr(xvHwX&qS4H@H0=n*l=}L2$l(9aWD73U=_AEQ^qg z_FAv))d)X}3)W7q%}_1CLleDEia9Dlc$3}qyatw>w$wAWp%=~P9l@JhNqGE%gih&P z+-ARPU9{O~PZOPdD&7h(pKwfvqFuD0;c*8jKD=YP91NHPwW_r~Nt9EkQcI zh?EgULM1OEtk?BtXr>MAo*Tpi4O@yb-fA!TPYu} zrzApew)cu*6*s_cz&w6p3%FDiYN!HPkl%X& zal68V={1NsjatP9>UU|xf@_oKoxd~h42zcO`qZtvzln7!R=8D}fHx|luvaLmI?f_W z)gs+0|C@NOmphaH*<}94j!aX%L1*kiv7Oe$*q{>!$JmnG-uY@4k1DX2yb(j!Xmj-O zz?I6cIq-T|;)QECVYu_5+hDYtkWQ|T!o^xL-IgFv@D5#=*vX-El*43`KL@0rKGts= zPE)_8bgQVFICWoAwW?nMi9eJj1gZcDnQyg2?W^+cl3+{chsPDZoj%&y;@>xIwEPD- z7ScLC{i|ET@T7y=^8XM%2x0e07=yvEUgxCz0>%fQYfLCXPA7lQK-f~MTmcfI|LBvp zVg32ep1*>TRK+XOzvw<#!obUW9J7=}>?;a)r9j1;{GS6I5Btl3{zv9Q07!q6zW@G- z7!R13r4D(NY_kC!Vf=fP<7y)B|BiWtO%VB?@Z$d-p#A#)v^y{w|A-wBj=szkm@X3i zo$z0K`0L`2h)l{pli#BglG2-Y%uD?_WAS?4r&=*y&>SixUNG(3-(PeO8T2MIO@*)h z-?ZlLO10Y>Gaw<1A3&PjUg&SKIJ|lPXR2sGlGDPFB^Sn#>pA~$G;lRU{?SHc$CY?4 z;bF9sIxYV3`Hnii+Ew z|3H<`t)gW1yOCcPS<&T`&SX%mz#yaMp5zuZQI2m{R2FBdTJD#f zE4*LilwPl*>_e|?(mrf)9V}g;Ywr+&_~H68+j}4Cs7W0zd~Izf(32TLHVo&(+hHTy z1(I*#l|?so3HOdm6M(y%?=O9}EZNcff1Ad8;9oaeRN47yUFr*4>{z4I``AI9y4rVj zGZ;zbhsO7^QGt(1$XD+N(qTpZPPeOO|1ZnW5>&oDF?ZJ8z?MV4-i!#t_?z8q%Ifv{ zz?7S^>$#eo%UMXCd8c08SM%>2R6X6Y>8~jE8_O7|VwgpFo+QdSq{gK+oZ5q>Y8{qT)Lk~l9Y5_=u$ZgFi)5$O~4XEue$df(ukCHw*M z_9W>g$Ed-Rw@cc0E^}j3t6d)5=?nP?Bg2qFD;+xaFy z8dN(w7#m6KU!7!F(?Z6Vs8`||Inq;ws6RIK6|=Ob86grhE-xn1xwUwdNfou%-{syMt;KGE$QU&Mq1(uvv zIMLvWhGVc=z~$yn(OB`OymV7r?s`>n;^^?lk{4QgXHTHm;En%yvt<%DMJCC~XlVYx z`J&-6IHVb`o+ERWiMrCm2o*~;R~|)WGa>ls-M;G$dI?#>7+rw~)EEX93&@tGTUEC9 zjMs9SX>GVOzX}8<5AgZazQKK**_T_fqW65YcWFUFcz;aaKR^C`;1Wefu+H4c#%V zxCKo*pMbdNk=xuNIOIFDvFZ)Y+9TGe^~0i`cXlCIjwrX#_7CKV0lUpS#WqpgQXb~l)S5hRgp z$DD-rN|0qYYn_^!8S{eTp_oUBH@Hsr>e`BDtnPl3*Us z^Rn0I1{b?F^@#6)txxuh+7mb&vyf0SLGQ@=CoBVKGFq2OIuZ z$Ca3bB3q~=Y%iG^NJMCT3;SyiW>O>X;nkU$v zk{Ab3$?agtYi*mXE}SgZn0)JZnX_K>mXG!tE(Z{Mdg{3!`BRoREq&{*qNpX;$+kza zyxHl*ch!r)=62UlhR_PXyo&qwzIu}$baWd}Gs9M>zrl5aDYe|zzwh>U{T|OjzvPfJ zdVsYz<`zeD+DvM0XUnmT#!%{lL1(X|UeeDso`3&z*&Y`{9_zd=Qiq5$td}Lr9 zKwJ@WUX2W-0@ZR{kjU<8pw>6_hD~u|n(LzAm&xy4Ztwy&w_}5tsS|VyAic(X3nLH7 zTNtFPUj$>V6~)XtJ6xzdpXaGl*)7>T)u}t51;0leE=KWtJ0aZ!d+$~S|-ol zwre~sU5Ua>?z!%roOar9>yg;<2AlYCu#vR3RdL#V%+*3~i`pW81;A5R*}v?gp9>xn zh0~4F0JP?HYD6b()jNJ2IW1+gp$A6)*rb!DR=%O0A$aF617($ znosOcdtT+P13DkIe)H{gz28VAAX$^e=FYBRE-zPK1wy~ zw|~kmp;xar8bVNK$Qpjch0+t!`?E1G8qp)sN4Saj@Jd-sjZ?6j&u2*+8h8J=&;_qE zxh}4xhlh`UA48O^=55qMD4b_mar0v{C2UeX*6Wr^Yg1wBS$BaaAd?)E!rbuw+8$W) zMGezlUs^mChBKU2c3h0VF{rv4p*QSq2c;U@f5J=gQ~T~|TXf8C?}iGIJHtkeI*I7> zV9@L;>Mid+!+RtUC{5@_5asXw>U*Ei#`L8F#;Q-+NqO@s&d$%tuUS+Y6Q&2Mj-q zQ1=p*P~}~}8ujAUaS=6KWJ>BKAUCr~x6~H!RkIVzBO@4&4{3dga38S)wBad^qpB!b zeK=dbmN-2xrm*AZS4!H88#`e2ZwL4iky)v9C+wI%4OS5&8Em%DsPy2oo zZ3#)4*zg2UHX=*h{_HPmC_5kaq9Tu->^Wv1Xa_|A1%uI|N_?Ag&cDknjhc zu4!5^1F+cr8U*psy@7mudEFZI$%GAy!oMczKd?w$sE&2LFiDsV=#5GPxSIu#z0~&6~YGGWUXt z+nk~{6~FrNwHTz>2_>kgiZz5Dl&HeIZH0I5l>q;{5H~!DA4WX<@FYxKZ}6(xav>Qz za0nxKZbhNDE#lpBWlZMeO zHF+qdU7385Va$15>ikaJsH&*t)0JPW#)a*#kn-O1LrAP`D*XT22$bT~WhDLSE@%K?tdurk1;>KB_dZ@F;-lezM^C zz+N=TlFI;Txb@}t=N6kVNUX`DSk2a&$nVpQu@$g1^6un36C_E$p;BWODTp}smdl`s zr!0BG@n|%2y(H2k%>8wid=-s{3U~^*X|umA|M)wERhouIx~28_9YwzHqPvEP!Ft7t z`{!_as-{Nm(2?eR=8D*-Z10Duf7CEmjkaNCPXpMS&j$#EXOJW1)jv>h_i`a5dI2T_FU(%KY#m$NbHm)HbG34S_A~aajwKOUx8(gf3$lEryy>+k0cjOx^im$ZNp*a|s#J7nIVaRjMSx@^vHIZB#c~cGGGcbtS92n7JgzK$ zs_0M9u6Y@ZN2j)hS3XP?>k1H@O9gN2XW( z<-zz^BJr!6h`$?=7E5yr!B}x_Lbi>5=dtHm541eNh#B+T7=pEe#H4IC$lhi?vKeaP z@ZA|G%iK#h7aa1d!t`7*BWu- z?FUrUw+lNwPm{KPNB2MurkxpGXl~R8_Lcmg)q$KQV#ZtZa^BbXJA0lm;jtpIJ-FNZ z63;Yk&*xgM;)%@3)F#j9yv)&y`Lsf0-$Wey^KUNo!hwbjEBJV zTM<(ZuL?>Wm5J~991c9Yj(xxTz6`-T8#qsR&9niET8Rs){~+M9Na|kUgz3M#YJu^c z5RKrEiS_v>W(Xw=;&YD@)s;2|dDX`?#q=CM6?Cl`a?u442Wxz0szn|v02sK;mXFC- z5jokQj4<=v4*p6O$_$ulz;z8!kqK(2FyIG-BB&L4eIQ8tz<$9#h(>|V(cl*4|E$>P zP0v}rfO$~(Gy=3!hkzEl>3)1(Ao!%K7PVQ!i=DQ1K%KJ3F|GjoL17;s<$tjY=)i}B z&XIwu6{-z@2JM*P-XU0|sG6D^X&})^Q;PGq7@RneLk8OY)@JoEgh%4>tb{X$s14jY z!px`2i|RC*!Bgas!{;inBS zGYKE&Eo3lU5>s<(B!AJJd?5-xW7t^8($mZ%QNFiF;~88^~h@cOH>*x zIS4}8tf8SFC~)+#kSOA)92;w6L3QMmR___^ntsa~0sFY|0Uh=<6_141i#0Xgyh3CN-EJs8uh!2TEA4GKHr+1>ts0m$0l(WA2 zg}!MB3uBm0_qK(lPgiQqHU^l!=;(3cu5FH@L-vL928bRoKfM=*};zcnozFt13g^<$}8<7d+$ zugkNklaM}VJ{*@)C32n1HM+n3u-mt#5D-9Ek8@vXIh)87tC2=X>y$ja#g0)LFS6L=FFva~_ue+y#K|tT?>4eC+J4R8_dQZ3IXFEbfzswXYS)$xh<0@C1+-}vw@vh_yel*vI$rV43#Ac*bmA|1=l9HM_XVwd+ zSu#((Jv1JCHeDt`9wsr;%u~O(@^LG}6M_bGn!oul(>r5;bKI)_IP=V|oG+(oC_MJP zJXu0r?p!3SUYY`&*ZDnBv0>L0o%bdH%KT*3Bjx<<@5Z?o-}KLahZE|jl`b-GM_ISd z0*D_obd;e~PpaI#win)iP0m*)~{hxA_Z0osy&il_{5#s^>4|4hcuXYbo{^%>& zJnwv%R26zw;1)vwT)^YDz;XU zHJUJL!AyjR+sDDjd`=ut#+$rqXBRd!cIR!Nfn%SKVdU@WQ;tATOwdJ2{Pn?E#>CUJ zA0H9oV20(=i(9Vu1!8yJ6XU#)Pb6%Z_^wwi{jstm@p_+>3nY@C8A4N!PqyECnsw;k znta6g&^8cT5ss9J`yML#(e4}B5M937uTCGn9&m*acjW0OfYVtIz)@8r^LSb=5tIa+ zO(6ic2cCMh4nF-P6fd&beA#e*$%)Bpj`69vS1AjrYCBDxMT+8Ih9t(Qk%%wK(}%Az ze-(9_kjQ315QT;}R$N6DUYsjj_NEA#>ndbc7QP)2 z=8=Hvh&V(i4UgRsd4OvP4XrLix<>z0NOw6nZPG6(%RSZk(<|Sa?GM8Qg(Fk8{il|#n;MFCRuA#tIZH{-2YgvqWoZBK z{5jMwy*YQ&2;1^TzVR zH3r{{s>ZlT%$_P5bR=S24$%xkXGs#@RJBb^PFvo6nc$T|Bk zDrRJj9rET~UX3wul(Bei^MqA(w2}>qDVW~-alP{8T%;B&&VF?=K{y@X{J@i;Hiy>j z_vp?RB(8Dz!o0y9x{!>FD0y!bvkY;fnj%@TAm>BEDcv$`FLHTIw1AxL*@g z7!qyS)&f%ro@Wb6{&CzoeP?z4Ml!i?#lNwZ<&jP?;4*H0)_0*RI-Ij7FoWaGM?iIr z1qZP@+k&4IYSI_(oF856gmOkUhiBYK0ji*S@7ZM^4rhLGd;>fA?W{Uu5mA|-Hvva= znbDPwOkVv-Bi$C{GQhgWd7EKKx#fi4GoD%L%GEi4!svr*uX1DH16B7VNoZ`y%55Oh z>jNXj!oE`e^PVrm4K^RrS!kXf z3|K52&VS=}5tDFh@^xCs;G5|t9oOZBFAj4&_**d!W|@rv7^I?^t*(|Os;FVtZV5+J zBzpPKn6J8D??2#E>f035Ooyzyv)FkJW_)CNB2lo9GkrY|wzSn68h=orth)PQM?9Od zQV-F&d8>L*)@a)2F8kstY`RvlwqpLO$Q}WUZw;HYtW*R*=d?iLMm_(o8UQ`Uw%YVe z-JRQ(8e=7&Q5FLjj<2v^GBHxnodOPi*;qBD(Rj8u@S7^Z|MX8j^1~%fK3-@ji#{)X z>b4Aa{;HijUGN-|Pm{^5Gz1L@oPkB&ou!sQFEMqdse?)DT|(;ku!DJ~-`U7;jNkZP zM}z3OuHIlK3B&0(`x%!`F=xYZwz2^-lRP|UJgvcHRYG}8b*6enkSwA(IicN`vC0h_ zh&~%jK=Q`@=5ykzGAHRYpn}Rq1BkjM7cU1FxZuB$*!A=yLo06ZIV?BDJNU-abe^qJ zl7jm}EQw1SA8+ec>5ZjU+WaB|>K_AmW&>UlDn)z%12QKvW9sEhU4 zwIgmMuRB5gFlm{4#J#vioZS8td+vm~Jl(?mMP*}gG_-$$jF8E1@U|oCql+^kf$_}5 zG%ZjYj!0sAKn~s&epz*QP5|bj(#5S#cQTa$%8;n`umFC*vNX8Tx9Q19iK~({yJ`fP zVx@__HO4ZWzQpF)48IB=@w$N8TvavW%XP}i-&Z>I~z z_+MISE!?52)kp*xeNt+|SgrRW4=03d#^cBxv5(q)T<}B4&AHfVldLNd`-S*C=0rDB z4-}JihJuD|srNdEr0X&|;s>+ZUD)u-5zzOQjJoBMdmNDqcx(7;@tQXJT`6;cyl37| zJ}%F6iB6G??I;w=_(~y~eOhd!`wS!URroJ%wxbpQ_V^J^E?>NEj~(uBhb9e5czR-t zi;9|^DsQ`+@t*`Pcx8ErGDJNWL#H<@jw3rj1Y1$LeSi@^Do`MHwhWX6SFv;1aMk^H9*C-WHSU*gy~ z&!mOszB-iWYb2##Z>MSg+>x#u**Ykw#g;7@_jsCRUQxp6FW1=GN&Vs8ul-oKo*P(s zaJi27ai}2&B_7^q4>#S#9S1mU_^y4Jpl4?K5L3Z$`uG_NSZfSyQv2*pn{1mimFADh z!Kbr?#+B`DA5&m+Z0*y-on3Y!ndxHZ0iexSS5#Ji87NLkQ7*Nl;X~~dz>ITEg6hq< zAAmtO3juPHTpjUS(=PQMogt|se>#<}tuh*?R=7N@L`Q1a^8ke!eufdpIIjGqRak&k zXL#LZJ;Q-4K5Kl;8#qmT7NlQ=`|1*DLNb|~MsiX)?Q!yQM*-4yw8%eyOoSb=w#W=B zWS?z$*3HE``X*b2=0bp)-59IJn`s4GgX^Qlz@!B!Q|8w{0p;c})oVIO+q&yl-)PZ^pLy`T*2T&gROyJkAoe9tsKrk_wGB;COq@y3*PsIiQV8f`~Gtz@X9Cs zrr^eGOgDz-Y$=jjfuewHnY^_0HB(~lo^Cwvq&&-guulI)8!YW#W#T}exoQf#ZAnTI zS8Ti|IuCpnd{>JSZOvx7a6tFBTi;8$_Dw)EmhiOMed1ekHl3XD*G)Uz@p3)KkSBnF z>Pew^=9fMA9vJ+m4mYP27)5zAGk7gY{}>5!1im2AU{)IZ1Mk+xX)PR>iWX()^vuv% zS=cCRbFgpO4SbfnhX=4A_;t8_Cr5@)I5WIGKfmnhkNJ~{$y;cxhbL~pbD$8Bo{-*A zv+^~fd~UYFieAGoG->BE1 zT5g%q3$AsO?N?kvARy~@q-Bo)vhJQe}Te2i=reZYHO_RiPniE~WM;}NP@8486~iptoN=wMleTO$5# zB#x17EP+Bp;kgPvW2a(PKU?a5q3H4DW04cT3VTr?kze<=PVi+*t80391_i-YBlYSs zk_l4g05jPEiCpeR{jkil+)pZc{svgboY+2MMUl=^bvZ}x%?vI_n|Q4!o@Gz6l>X0%IHD0vN^+))vp0ER>L04*SWUZoAJ;3TeKCZr5xF&MY%V?wSKZ7kj?aM-;f+CY zy?qbYGk?@^qgvQcYw}hT>FS03m|CRRmCGkbe-yW`Z6uPv_bh2F2fH1ThNVgA^rb3^yM zx3baAeRowy3kLhge3K9Sd;;Mtuq^0AUAz7M&9MRjTVq4lH2`X8l89Rz@^?&PW2wI+ z0eBrl%}{U+hM?A5-r&r5?+BzOb}~onQ%@Ef^=DRqy%vhPryda0=#|8NLSB22;0=Qz ziela4nFK9G`5DDG-P_xni21H!?Wam|dqBY+T(t08?j+@1cXf(0;OE}sU>Nn`vCBv@ zaC+gbXo~BnlVT_Gk{2T{gN^edFD;Uy zK+5?XdrQncq~Nm1vy)pAyYqDflA2$GC3#oZ*248YV4+n^uJrI+zkn+A*V%&$!&#iX zL^cbya}e@QL#W!(tGC0$AS|Foo1%Nz>RP4sYMy`ka`YJ}a~~Tn8iAN!ff29;kK>Dl zm*oc>kn6dc=Tx{pT;UO28{XyKgIGoR&9zd-ZpbELn=C5naDx^TC9yl4_hh(9OgQY} z#m_CY{&OBgoU&0ZpgIXFqvLIq<6b{r88{kl@W9qMkD64V_YDzMc!HOI)d#@jRNA@& zR&`}Ca1)KPI``CO&A^98&-t@kj$D^9QCbR;Qn}$tv=c{k@0*q}{aC6=UP6ZDYHKT` zx5u?zsMo3z0R@#bJU_Bi8|9EpX7LtSaGt%|V$-{;7NJ@PYx1%Z4fQa)$5{qPXO1k@ z91OZ6fHJQ%nyx+$6P@}ma#3*HxsjrRczNQc`tGogeIrh0!UUbof4S77Gp?kl*JK%Q z2v%!!hwm*JVwH%*8oZ0^d1ZFS8Bbb8=OXmH{kujf zT-{rbM<@-hGJd)&O934nk&XD7cI;)mrEF@7*}4wuB30FcYpQ(zYA7Z+6f~yDsxd2P(jKifE-*>F7-1KSW=^BG4MDEL=5jNs6 z&wwgDem>a;>oNIfS%p_A?bexxgb^iSkDM1_X-R9L?8rKSzK;^SUHy5Kg3L9CX>DdU zmOXN+IL+_vtceAvLgP?GvDFzGFK>RRJ^dD&BLY5~Mvbmp9P#l3F2$-rKB79qm=82C zI?HLPULx?NBju!o*$n}LWrW_^54v?i0#}D>>G$X4HTn3hM~X2~5>j4#;&kQWtNTAW z$ry?FHSq2o`EZz9BC;y$_QkN9-k(c}|Bcv~#7gAGX!OUD6NT>O;P)#UP> z`4hhPi=nvW4EBtN3C#Q4W?R@cP2Tbl47M{vy*N4%jn2enVOjh7+!V$efAW%>yYIJN z0n0uu_5AB`(f*$u-#a{R`s!Bpcvv^HCkP3pWrBh?72U9M-kvlVzp{_SVcQhm)i=bT zR8n1!L2@ix5}tl4e{?uF=bqidicb9$cG>}Lnz-22XS$7v3rX!B>W#Lm zS_pg(L#;1wvM2=7W-0W21#Z}H+es5b!B39Afi{iwoJJdDxE|v)I@iI+LOj?|BhjnL zmDvnYs+Xw8eaPHvJo~E&-!^r4h}8w;2(%R!Jor8ADSJMev*<6ZWM267f&S<2PJp=( zg=ii!_U`@KR&K7@^}E{<%m7lFbch^jC3PCtvoyIJ$zUx0rP0y;~0w}*Lx@>rH6L&JVQxJ-z= z^}~B2?*>v~b1Bb4;q8jS1(1+M&V6-dE!@c7^y_s&_|arcdcrq+cIc@NTd}XYsMtRwPviZRD`=Es3XIY{;)9-&!y3=ow36z`utn@_D*7=NV6>TD53Q8n>nN*T; zG$eh>fL(2TG+#3PLlGdVGlirSjLYAQmCGm6fHr$!RvTr%?^++*P}*ef550Az!D8Sb z%=ehuWdF3>$!cqDt57sHIq_%(4I5XtHs4k~jB7JzF@@V#ENGGtya<=x~Jn<3u z;oH*YM%*j>js7thRqrV%l%Kt!Jq#TKD}>jcKP!=brkQNX?~%-YN^?9AK|$*QkVwxj z3CwH_m8)2ifU>yv6}~yJ_a1G^gpaWajPJ@$;@gDGck<}tFteX5*0~TH{{wN0Wg07l z+DQ4)JI*X?*mrN^vYXS^IE~eeTx9+#<2n{ry^1ka17@D$NhOne08CEA#+1 znpN__eBqaeRchx@$9UAqCj*2n!4XGmYImCL;g}09B`x0olG2*53hWrXCrn|XCkjmo zNsAPdz(sd|Q|(l!i$hYLw6X@jCGE~PZ8P#f?$~Qa70=^M{Z*fGBxglTR>Rt4@akZ9 zA^Yoj#oF*g-uqOp0Ng2W;uqcQmDhLe*JQ_Mcw8c>n@5i+u12fZV>YADU2iDYiyz(8 z4O?*nrUcfFMCB%oUeV^l>C?kvZb54gh4yv#A*ZNKcGeO}--LE_*K&CSwm z?hkoE%?CLLNk_YuB94RyS&&eVRE>`s`v|%jfLkcAqsk0p$7Ay}i`j^-c(3VoiQRgw zi+?@$Cc)|>Xs)nEbXfwXas;_P$qcsM~(o`0cG6Y~{X!zUS4QH_Ck79}+iGS~aUq|}& z>-Qy7snJxLA;PKpQTv(I`b<>4^fB;wxz|6vw?HjEKLKFleaAbN?~-fhW&351VpIg>>{|zpr1xUX5 zq8H55)aPU)v_}jc3vmA+YwDYq&G{go=6(QN>WGTMk=8OwBFXq~eh-r|CO%WXJ5w_r zvXw6|>VUP>X!sS3h~20mD)bN2N9BUa&1 zQ*EmuO@3~|@RVYuI3Q!_ucZVi1HgVct1;rWUM4K$nmS05^KY#eq(4yi8$(lNvP&uxX!+E+P7_amHyg%`61`9@Rc5ixGUq?4T zbh+RlbGBda?|_@_w|;3YRw`9VndjoK$Jal~*+eeZpQVp){K-VhVhMxA4?|b1U|YO6 z`u+h+pz;l}J|X>Alz9?LGtzbc`Z7itTdOYbc%1la%3LaAjxI$SAMUTDV6{08ds1>Xm65g9fIk4hsB#`8uGm1ZSf6h#30q<`x2ti+$hx1u|)KAwP@^46AqQ z&D0U|DrJ#dD8Lsh+{eyYVsk(4zw94?&=ZPk`x=^{)$L*oiZRlkN@!9T!`@Buzq!JZ ze)lZhIz#zyz+Z@6oBO{DDVhw0{=Z))P(t}JL;XL)dB1-9-!_?pfd3bRfu)K9;KLNW zyqlkb1E%NpHhhVBpx9lec~|gCPs!(*^RA00fJQ`T+u!*rI8r-=y-9EYnrK8LUgzmt zg*z%AXPsnu53E_uuj*n86x-LF3$u%_YCVrvQU0hx&-d{C-$SI)NMbHTaBiIdX$?Qs82DNRNqH-d#4df z?4=>k!|qnV=);tqiBa-OswgGq`M!7P0*g0$r3J-Ul`74{YOEN>?eGqjxGXdeJJwYfA6pCK1%`!21YPxc>fLY)chi(m$>cxJ3QeYHb8fkUp^6_3tf6>yE$jCOv^o z_|%K|x86xZeu*BMQb>8N3Hw}cFl9p^aUOqcu|F~qkECTGLJ)b|jZp^CLx0p3dA=kd zRA+D{T*R3f-myXyY;?|9aG(pY8xs|9#}jy(1! zyHrV}7^WdUpIOWiLG03QL&(IBT?{_}pA6W)VOg zw!!1Zls$%XgPPzxYcMaZt}LJlkq$SyJX>u!I5On;yoC2=vGpJY<8Jq(J}Y)m%=*dn zgkfn|^P6L-R#^ek#by-Wd+pKFDB=1&zrJ5v>=?O_p$)b0MCVA*OCRwS`E+-C2EXuY zMCFaXsmkUTqzh#nQ0&Kuao))Mc@eG43U8Q_v+*p1%0UO-^VwIvr91ISp5f=V9*4%s zst{)X7}OgKutV(Gxe6!+-ZxerR+faD%>kD4Y0eB6+Y5aEzKKj}f4$z0(X6W0 zG9&da>fQGbtEAhWIu?(Q>zx|{Q6yAd6HDJi#~7clHpcCD+1@EjiD)`K#D=YIeNT~z z|GdZ2*2z<>fd_~L^UU@;a(_y`#ZPLY$P>r+T{+ONsjv1o0-S?sq3f2WFSncvY(5S@ zJpAlo4l8Z(_^w^IIJ$@FZwedsM1x1RfQ#zzudkEZK5o(j?0>>Ld0=TkZZz=|E4~cr zY~e-lynqG&5JzS~u{vL77WO&Ep@%CZ>JucdXjXT^opJYEWB(S|*ynh&eFFT7>FFJZ zo#0NMPO29nuUG{#M2Fg+DeWnZKU&N&*bksfRUZ>Z=C({%xQE2tQy_X%o`dztc&X&ZvY2XBY5jPJ!<@#KyxnBzx}zRRCYXh!1*B7maZ_N~b&7_gA_wRY{14 zIKmNUvG_(%FsF&WL{?@4K5EYsj37eVwG1PEgm!I3Tr(0+!`?JHN} z1&oC-Qz1p--Z)S71-wkGT5N7_tUzYQ%>+zK$u}%MV z`2L7O8HmyZT)+5P@f+CZhNUzkrS%N0((=e+a=k1|$RF{7uS$oHfRwLVY}=Y@QPmAv zPmzpJkRr2sv6s!jkXi)mR@i?k5k(~KoX$FDFTH$e*&7^|a5NZ~FT5WA;eGolu2y&W zkleA@)ci~9If+}bIt9g_<5%Ch5N&5KH#g(W7?Pj^*+ca-cC^O2dR=U ze+TRLXbm>V2rz?G9L}<)TV+rk*my%%HWAZ%DC1}9D{*)A@APFhMz^i+oycgD&KBTO z>!JtI2xEG@JZ&=xHWVHzM;=YDZMX;{DVd(()?39BD}rfX{k%fJ=J(w6yot%;UT1E8 zxvj{~<8B=7V`a+!J{(64wOP=?fpg9CU*7~u&dBg<>TnRFiTaco=Ehe(JF4>m%}nUh zr&vN?7h8csQw~Y`IRpKtYp3d;VvzHh1z{;u!RJbcT~QwBi(dvC^KniLMXF>bg7V*! zMb~G(B2gH(<{52jFgN*rgIaSr`vGcD!NqL~P!N7kd4o=YuPH?9x*p6RZ9Fm|jHVds zA}$a@c%lD{VIpgAaCH-0f0It{tdZyEVy&4qh82@f;CQ3NYoR#J{NC{?ETHmphu7@c z@8f3PO?Yx9GpNv97{Zsd%S|9(C(o`nc&#U_C-rqSDhECd?wJGq;k@I|hCjkqpPGYA z{EkNjJl5T9(xAz)hwdzv1;xlY*NZLcufD(;VIb=gK*(Yj9HavRC3%pK=yctk1oG-HFrOB+%ze8&E*?y`pq30cg zd^8-aNXE@CjEDN4kzS5$CGJ-zBQ$aTFV^0&E3T&N8ciTb@DSVy!QI^n1PI=^1_F~>OZ1a$`zn0%>)wS6g*|i$otWZG9({GTEzcJ|%A^`YP2q-W^L>22s$7&Z9>={b zY|5GIO@Pj;3Hf_$Bq>J=m6I{gl;{1=;ALZ0>6Vlz|o+(T30XKb{A!HA^vLDViCvX*c0#O-gNhPhUi|yndjJt?RT@f?I>-TnL&Uip_HLDj~_LL#5K(Dit^g+ zN{zua`Gnuc7-Ml*ols22+$Nc4&EfVzcmRoBc7=`JX_*Yu_mrTno-kvUkHnpi@wEACbOAg0NsbYTvyaD zRcU141ivJ>y>)SgC^d{IAvLiE$4l4S7s1F{!X;Fau!OfyfOjKFtofe1P44b@R?Qc? zvmL1qfpE?aCnL|!oD{9%RLUY|)2F@#xU-4|7m@H5ZZzim6{07XTs_Od3R5E~nc}D* zi;m^gr)z8QiqORR@H8^kx!WmjGdn`pMh6Rf^7&t{F#raimy_vJ<^RP zuXTW!X@FUs#h1zgI02&Z!y@Y3BIQEu9%LAphT=4wMebdo7is0WW*0EG`G$BYcz+J6 z5JN3O+%g&64C>^iLk(R!-pe38Q#5TDa8oAswN1sLqk+guHDl$!07FD&4O1jUGg5Z^ z{K6FyPxSyk`*wj%lo=EJaq((MHj>=#*6ov>LuYq%#`@l~AEk@KsWH->;!xl4!0P#gf*mEc;-`&w0nqkpw3Njy|2YQFSNpp!Ncfi&x*eO5zdrPV$uF)UPlegVeEbPz8?8NYT~SlUqXS6S zY(Fz96EXysN7HKd39*Ngf&*?6~ zI$q^JGkUAiyf4|`U>4p@+61ZG^~lu4*xC`7P_*`}VRFPx={ox5)VS_N z&?hXE?vV(}-XqL}ynMSqr=A`CXp!2H>Uay6@!nio0h*W9s@s6g+0l1Ocbm_E@8Z>S zs(Mr(`@|b+#AvkOB_!)xI&*4n;KkKb6e8qm0WBT(j__{rdN6WjIQ3#*$#if}Jke*5 zNwEEq8a(g_I(@zETd^SD8~j9Jx%R%EYQOD~XrA=>=(Ybd6ip#UzIe?+1#W5c6V|^B z@YPR|u{9bg_k+kPM|Zc`#wo*_pYzwRZ){Rnk16iNR^|JH_^zK%Tx1DHgYnA})^-2+ zxsgG9Cfqk0Fcs^U^-;u8jit|qhvmNs>Hbs#qyd0I3i@lm5|4jbX>SL`?-o{(kP~6# zY|+oJ_Qeet+vpq`h^2GAwvo{=-0wW)8#6!}NEM#;oP) z5W(8$SDC~yVca^}zmso7`EC60q~&n@5o0s!M%T3|YajmR@4aUt`~TZSn@#ZZw`$mb zX>W*~(0}9o`Dys=`~Q`Wpn@yk(I-G?TXw(H`B9mNF>RR_$klcjeY|x}XJXiSSP43Q z^1xaFf-CU0dlPLw((@^gqC^ae;cuKW>}IUiOu*^Y9ElYy-8*`d+ABE__)JYCk*B4? ztBl`#()VUSy;yLmyjLTvRAY}M*GQ@!>F$4GeVjK3`092m4>quP*uK?dix<8FY}QE~ z=&t?p#9BS!Rp{WNkqI5mnQ`LfP~JOt8|PD6`%1_OBa!7#$c!bhVoWye1$Nv`r`gP6 zSCKvPnd;qLFP{}imcCm24WS_;?rn|-$K+)Y{g_}@!TkZ!4yt|@<< z)TUn%W`osu#ExI09v5GBIT155F(K!rra_%&SCDkRTsziEU()JqWOohc%Fb#`!O>9W zpHUd=ifIdM?I4c|eyu_)!oivB6q7HaX|EMS{OF8*HFPdwls-LCo5QVEUEs5@uzPLK z(fEKEQ&k|pq+Cj z?LF)AoFHy^!vDY1tjOj*W!$8>=vZ+$V}asignNi%(#hQ)DeV(2o}9KZgB;+Babd_b zIfzK0&hau%#jR-kDv3{d!lf`V;d68dAr3*Dwxk=e+ve6KLq=9n!LYtVblCn88&ux& zvJ?1}eQxvy)M3*Qto&6L>ivqwW3KVHJv;SZA4H`MPr z>X)RfeEP4{BBH%u@gP)GeG3KZ79PYEmtK~X>BahcbxH{vk!af;6Vyh3>ua3PvNF@wr}y|r@@eP3*&;ESpjhvP8hZ@S(PG%Qa8|@ECozKwQTu`p@xtG z#;VkpUpnm+uyaoOHiWEYYtm=dOdwNTISxn1ec7?`*)^4HMT`P~x@juSp%o7@NaF+tzVWmXojoQ-!9Nucr z3{|Erd7KI{w1K&8Qd>I8t0<<0_l_w%V#`Kl7!Y=m&MA1@pkZL$*zLQ{i}e^$UMUKG z*`^~-mhmj{xbV^z+B`JITwIstK?%XcLE-C4aG5N0j2rR!^D|E@)C=3NziVCs*({-r&`Rdyx2Cr)L*zc(Z?-sj=$ATLONw@1|KID>jtJLt6h^($#H#!SsCN1eX%y=oW-y zn~PP=OiUB+rL~~uu{gZRVHD)(kfYP;PiIVa1_ij$qMgZMuZ|O#bcl89)747$n%A~5r>F4QSLW>8dIb-dC z%-{NSx0TT_QO|yrVC0JHf&c==g~gvRZLXQ8oBjORxt+YY@~4{1lO)rW=kpI6-SAC! zkWH(wyheCl*_!A2XM3D@IGGhOxm?-r)c?P;uv%|5IcmeRBFkO*W&r)PH%)UMHg^D% z#Udxr(G@{PWXu8VA8U)H^Hku2Jg>-0LsQm0>QI*ZGgWWv7Sq)u-U{C-G4$SnU3-CK$2+$Wr7#8V=R&>|eC~ZVI z0MI$N5DIccN}OD(;*5%Zv>;GLUez2ELo|+%k+{OGUlLcuT|h?j9PRd79chmK+bHp8 z`f-v)*z1Q-pjb4KIKjX8)qfTkLTmTr();6#(!A|oK==E}{~tBu|Hc>N)@xlNL=y>F zi7q;-`Q~0ne=aqIg}?H0_nEkiu@Tl-^|H_&HSJ`t@+hjT*L-0!ZqH|XA>?rdF1pf* zx<%8^}k~oa`k&Fz7FFBpfyBPRxWe4AQn)cpW)uE!6z3+Kizk0 z(tVrll#i95?2zVsmJ@gphywec=h((0EGm(y@gDGct;U5A!c@SbUM>1WvXvhPFQv<9 zJTW`bL6S1SjWdqDGdC@)@Ch+y!UK|Co_oss{f&w((%x+}+mrQ#$SvPSjCB1CV8%Uofj<{p6)qVndCI)gzt$2Bn1R1b zDN(i4V14g)M(uEbwhYD>x>Iq4zhe8D;SyKo{W5wQPdP}tLpXruLizmYoMk#jLgU(! zwM%a6aH4NCt|3~>9cWd&M+#8S^2h^EY!2)_4}OQ z3RiTFua3_eYdD@2x7gWI?}O)2V^Cf0MqE3D@L|Ac2eVrbZy&^3I%`b9KS zTL~0GCcvb-vO)aX<|Nr=XndmkB0iWUg((2W|s{YoNnm4+DG8wR(z`%@;9M z;kC9GXW9UlW-naUQAd)oLaIoRrd{m`Y@dII^n}Rro0PT+#DGpJ5i}ZgejIc*9k$Pi z%JVmNG*e~2at`1n8u$4ndh@GqbP-2_h~nGgWG&%n3bx2K)?ASuZ~ul;$B&Y--CU>0 z3C{>huc;|am^rnGn<$D&-bRYsVqEnkmFapvQZLdSofL_ ziI(#M%P-Jw+bjYEy#CKHJiu5)SuoUWCWC|Z7L4mk()FI)DZ?A3fMNHXWjl#Sqx*75 z%B+3y!_8>KH-f4U+6xL8&{ob-`p4Jwg=z&Hm~&*Q%eCqL&}K7*>uGEtt+C?P{+QJHBY_(5OoV>1zEwX6G^*N;}b&1LJ7bJ_RJ?()?NnVHF zsCBx?Bk7OIkF`HoJ_Gv4mbyW+Py~vojDK&r#=buYSi`3_A2N58SX4dkFCN9PI^y3b zF!|lFM;0g*Dnv;L(_iC$J%ekErug>D_@w6h#!V42e{2Zfm?=e}>8gH{vTaMY>UNlK z@s&M(@l36KyBqWKXW)7*Iz{YznE1}kVI0h~RnZnME%`X7qhC-6Z|&pJiVHIvIgNXF z_l?;vE^oS(0%vQ2T4(@;bAb|YPT8grZ}>)GrJDDc@R;Aa^>fUhHQ3Qu~-Ti*nVyl!>9bZHgn4;Fi+jXK)s*{RAzS!a1YDcEgwV9gP4`gW)};IXY|L{LHzTx(17HcjHP zThx)s=yYpg0;`I1E}}&ueA{=^{%($%j;rROqCA7pBL1CiT)Z=;U}2use(=RYG{CG+ zw~Q%T>7e}0>1>EFBXU+wLwup~3`Ads&6VBldjBUyqMS2HN}9QI>`W=(8%-y_3dH;6(>BhTe_M;$8EXfCSq-9iC0k*E#$Qrnz*PnZ<=GYd z9|*!^KfR`Yq+M5r2g`SU7y|05R-FvmDq8GczhKi?-|^HN`zfV?9ZD|z^`i{eg5I%p zHc7$9BOz#RYfLp^syj;t70guE(HbhJ_7z0Iz>h0~pNuiOipNoVF zf;2w{wZC%QVbO>^-3XY=k?%~dfy}@p{?lgPOTAI=?y0fg-wtQTR__rz-ImLQb&j`k zG@($&u(uWiC@jxS`WZFH{OHqMs+4h_2fT&O)&_<+Ts~+Oa6meu_S-O%QngbPzv{Qk zo*{g9z*|IIm>sS?*1{&UQ2}M_k(hH9OlO(n)M)`q9~Y{q^A5iSN)`-GW-7{?KY(jK z?99bIYBbxaXcgtz(G2_i<6B1`>HqPu3YM~8u<+$DlZ1lCI@lN+LCiIke@5UyzP`w%4=wf7@M@#zN!(RwX zib=P-YyO!oZM>A8a_HKk^O^r5%k9`hAh9{egs77?v!HAz?5?27LKh0=ylb``H$-We zsGfL2bb)p8X0OHLVTIz;b!*{9GvQhCLMQ(Z%9R|1@u&#|XV!z2Jc4h&KU;{iB@m6B zJYI0Udoy_7EcHh_rf}uqG8Mv{YBxlLZ^v?f!n(S(ZsGn>CKg7-Z=wmpVHHU1`Jh?U zbIUeAo;Cg_gh?C=NX@_c5pP}Jbo!?Um})?y13B$wWW@~#&7J(=zYP4P>}?nZ3|YE9`jYm zlGzlktT4e#5CUE|tO zYqNhv;lfPTL80H3>u>pyTZB}9oXmu7BRcw0vpeB>IFund$uQ>=s$xxl5SG4Pcxd8; zxHLq$*k*^7;awB=5r%UUk5L$O*7qth|G;Cb-P+D1>Icnw)~V=(1y2IjVQo(-Z@fSu`CLrrQxo%d~ScMOt9~*WTbSw*htPT_L7Ou372(mUU+}cU3mFq&JtTZxzsqB z+DE8#=w#vuispH*)ql7lLL|F`g{=c;sk?P;sLx>Zjxh8{wTH$yhLbcgAk`c2g=?f$^u{ z5j4>&9ZZ26>hwkhi&-0^n!xk81}ChE-7_p>1pr ztp;R4hYOK2;*L$V*x)04r=O<$1vLT+(A(r_Y z4P3QKMIJiD$cdvA4iW-MM@P9DGmuXxsG%xyQ9d7MWEUei=29>TiQlV4S|v z1&HESQYurfARK051$&%R;K=v*%w=UA8H)%|6(7#rW-s%HjYW2Z`E`Vi7d>TXr8Jkz zyS*#W(Hd12=&7hKfiS$dxGFcB$rb-A8aJj7D^D7q-@iR+kQHLaVKxJmzL;TxjkxxE=C1@Oq)RCdT2ou-)TdXCygQ9HRI9@&YqdAD%x~;<=EB~(*6>RP%7l4*6 zCP|)=@DZuP`19(5nD3hJT2_{+o?_=%Df(g7^r&u@dAdKnB#KI}*)BIQ=xQDRoiLHn zv^{;{G32^5KESgXg+Ug!HI*P2gQkF2{>mP-`cqC=(B6U@-g6w4Q5tortu5%{7g4?D zJep3?@xNPoxye*Gqg;KE&o%s`sS}@Hcj5j;B&Za3i~VX^aj3%&RkWTy7UyY*!a~Oq zhu0NeN&|_S-2SA9Mq?U%9fu>nCvy5sZBT%4mAdelJA*^Y)4fWvr%o|r*&!Ha z19I4!Z@GVo!dBO@Q8K`e>7X#Jkf4!Ph_qlbs95L8IJ=le%>oZKsCah~XmnI#_65`5 z)oHL+L_ABj7>0%ZOHUxX5G{&700@il$|>5xV#MF79R`t#8wL-ffOCugxkmG?eppX8 zjv>mZYPS)_(HBcx-ks|(!3(YrlcjLbj}F_tY%s9v^kB@zrvI|+}N|57t?yCoN zTz~IqNv!u=lF=bogI72|0=X^F{dDAuU06nATnmtqi4 zFD*e&%h@pkRGMliIBvqI$j~ntEwB~4;WE>hfxy{}m@qO6Ri=0l6*z8=KqUyz6t@kX zCMAQ!K)$~yWBHq%j`)ap<#+zWOIC-=W^nqi;m74(MahFZ)luiflAKlcCR0I51rEZdY3$c9Y7Na)H2z@FAMApI_>xmzj z46o1GQ!8|uXmja3r`R?`_=VtDhM)hHEy!fyq! z1BcIHHQrI}s-JzvJ`^+TJ_kg^HtEfDmZ!Ba4SL6Ut?Q2e;z;rQC_uh~LPZ0d!i<%3 zwnffkTC=u`JWObKeE0*+c26xQ7)3l8JXAB-S=0g1PW*^=iHm#)GX=} z^x?Bgpl!xUoR;UczMf*2e(Sh@4Z*f}NS{z?9SIo+Ncahm_SUHqRPUOKTcE%N;tnM~`^(3%lIkA$oR4a`^O%7)!k{{8?r2Ar z?7_kybb>DfmQD7yZB$JhtnmKSBD_)?hk4lS zX#`;XGrw=v$ncCu2m&S^dsaR5v5~XbpNMX?+e&TnV3NQjd56c7tETE`4W*XP`J@V$ zZ`rLs3E7l4>2{pSDBcmcokt=AflCyqXPjY}j7hJH;@%-P9uIV@HsvEcXE^bt!ipds z%?9Pw%XkK@D>2%9A`NH^y4ePhcO8p0k9~!4CsIEea&djk$w|co$CsbeKJ)j5S;V2h zREVi}WtN}FvuHJPAyqH3BLGX_&0TSecFJtzFyZj!jx1!R2WzmEA@Q>?FLzqo>(t`m z>AqZ#I6fq)(8b>cnKE*Qs!V2kG6z1qb_@)ja6xx`{K!>h${5KjkOv_vo6`3pbK8>3 z?faaaEV1P)Ql1v+BaJ{5_vZwJJKc%$#HcSa|<$A8=HYb;9we#@0e^RpI6mO z&>6sV==I}9$5Sfdg!4$w2CM?ld8ms{*LzACl@tlmwbq&X*X?}VmC(1(X@g;FT`_sM zgVVZy(z(4EWP}F-f;65G3ES1Ho^r!IK~gc-yh9!sl=w1kotwhAFDgzH2=ZMepMcrf zrpxMYiBmPxRKXh+#d3M$ISJusbB;)6Q+KW^aECmYX~;JvrkU{_{&fXx_2efQJQxJa zNjz1WJx%=7b@H)ZzS<1Kk^JAOiaIR5oj_>Zcxs>vzlu)>On&5Il9%86G3(&<9`7D|MYd zd3|C$o9HE2P8ODH8>J0ua53l_swsF=b%5@?3ICq3+jhMLxEGB0RdRuOp+sipf#+J8 z%X=J+(hSul9vhEndj+-MhN%Dw`GIdBB!PXpe|-B=T|L9k`f4^#eWnZ-_vF)mOSw4BRzLqq6dH)|rk-_{LRGCl6vjJcYEvNx9Xw)FWyyPe0V z>uy{)oapw`FI(#>l6l{4Z&b#15IiQwxZyTx^e&h3=fOCk-K??JbH4N;LNI7~#5m}j z#_Nn>xgGC;#6V(AbSszb~iw7U#Nd zl%?ZoMYmtQJ&Nc1#ufrs={tVz&1F;)-EE&^huE7?7Ng*V%uFqP?mOX{vxu}GwyLgS zpVS}XO8pw>)MgvDA!HPZqJgA7Fk@RXhY_p)rRIc+LhjqS02d5fZ^I8>Eo7lCo^8&3 zc~BQNsAM~8sDuNv1-YBdfSY>i$6RPy%bQTY^oEEg!E&-sr186nXHVb*ui44tcSSzi z5+~(jQ}Zu_H**q_Uu7uV4v0OTSC~awoG6}BNfahRf4r{mb_^psqK%K` zBmg8pM*-~?t~6vnxQ>7)NyuVXr=CEnsgrhJ@eIFky`G*lbR_X*g=l+vT9WGuRgjL4 z3iB_@wFiKt{g#UpOJYNQ`F7otnJYi@rAL2=jK2v@)tOOil}|n*f9Co){+;3h@=n{^ zVGW$#<4LO86TmeSQ)#)cb`j5r0pVwCp{$*L)C2wsuM0oOG-lj2HR(G6fzaU>SO*EB z`Fr+WdXIGIz*y5uAxY&7pncgsSB5MXJ;vw$ES2{Id4S_00K&kUD-R?zkjj~DVCGh} zIvWuleo4$tpOA*#D#~&|&QQ#7N7tQGeW=T2?g~zny!hs5G+Gy~#^7=7zTrqt@7jnf zNk}f@a|aXV$j#$dZqJf46As=W0yylKx$V(9^Y@gYjO?UkWtDGQrUDe+BKf|9qwXz@ zq+vHMlidgHUX!$9 z^VvWkwhBoN493SATV96GVi2vBTIhU}D zmN>RjJv-(Y?^Wn{3>wv<0QO@p+k`rw!MtV8k7$Qsz}k4 zv_T@Q`<^~hT~zZIzjwxOx(+#Mq5?foYu@l-(^$5DCDXvsWY;iX@n=FrY#6UBLpe@_ zY#}UWp5gnz6t;#gb>EC^J$Z~POB9Su<;>9K^h$8t_=6H2#0kdT_V&JvDfLblQ<#P# ze7%cHl@55L1!cCQSQ$(h=0$eh&U?uYMOBzTeZ6&*sk;qf4%)%ZNAo;|pKnFpg{$it zY2g7kw|^#B^&P8?)w(l!2ZvF2vg$(MJc}K-$ z0rm184t4U!d9X(%)d%H3zcj(i;qYf;<)I_6NA)=32?oYoSG4QJC8WP~8oFf)!M@Uf zE?GoOumR!958G}C>`z@jO7EGNpC*|67om?se9fDZ6@e#x3Cs?(atucRFmV;`B~L(m z9o>|~g_P*`olh#r%Ok@Qpy0c59~tI;{+W(0p(4)mtJ`CMc-R0M7;6A-rjrM0QhkkY{9Qt!Kn7sU<$yn4FO`HI8 zAJ$EvahZ+9l%}MB9ZtgKJQp?W>TzAKjXrd!1Zrl$TB!R`bcw96%la*!G+{eu?8$anhN4kBhMJEKP!d?l&{`c?$CxT>#svoZRT zm8zc_vmt2yu+wO;o$=!?S;4BI+$ZRCEai>&6|Sz z<Scrt{SjAYGQ@&xLx`Cl~)uY z7DrWh()E0dUzPQs!v5a3cH>IVAYwq>y|I^b&31JX%+?gd!5Ig zyQcC-^mXqgPVNF86n;&$+bcoHnmieh!)`d3agWupuW^ZigWmI3iZ^q@E$reG{DQ7Q z)BW~L(-mBT(y@_=wMO@jw+7d4og?2X{>uP&25Qeu8~TDwdUzHG!&uQw7`qszYm>t>% z-1@-pb*hC0ICc5Pcw{82&P!gN#cO$0@mQlrPbELUAwUqSpr|9mg$})V&bIbkr?LUw z?N35|eIr2){KQ}>8MUBE&uGD3df{t`r3fo{)6;?aBHVdu^x5&E)QHYTw(HJjlmFy# z8FnlCbxLETu;S@aK;)skU13_C$VQOAEC&)}OEkgigW_pBWky5-s|k7<57 zX%v0_vK%%6S6MyTO-~)J^sv%GQf*pz)0hs2#2f>620QLQ;I2#jH#+9Ba=FveY?I7d z(!k7-p3QocRV#7;C7>zKo`=zovrYaZPI(^kOcX9`?k#2fJ#V#3&t?;@U#(eDo$XiE z9?C}=wzVU=GwQy2j9Q8;Uh|_dntHHCvRf9brKDSn;Fd!}fAd15$EGW#cFA8D>oMgw ziw{(bRR5;AqzZujW_iN!^qS9;7xe~?$8X5Cm9kI<6 z+QlTVFv2`2!Nh@SZ9z?%Wv7b0$Tfe-pi4#Ya>bl^uFKVYBh`7j?(h1lGX5Zna7}vX z3_abF<0Y0^6meO!mNvD-088uRvbrF(R*Zr7Tdqr9YZj>UKPx6PZ#VqjH_dBItBcZ{ zC~zsLzU={SZHF=-+eUR%aNHO@=5zm52qW$FmA$xl3r`y0VooQIFKcf##$hIc>&5~r z0eP$3QPd1gl?NY4=9y+bH;IhCj$*g4;GD)$=yYSyx{6838gw2vK%FB(uz+@Yhoy4r zj=w(4FMb~JcX!DBcdB%2G+mCz6T6n|StgTUOx$I0wAv6k@kp>33ici(roL;N-} zlJhUxy_oQ)DZ~F~_cm3c`t;CN9=}5YQnHp>o%x)Clxa+;70c0QsiP;1b!W51g$4oA z`#DcwP~+=zxrDO6+0%mnzWn4bE%0gz!BH2PcZ6p|I-l=1yg}Z zI0aXGYnowyhF1$OX+}pIMatOZS{1UnjKHpYL7-=oRR1cQj`_2@MXB)4Ss?4fMy4g| zy4dzD<;Jgg&-sy`F3*zn{4PqD;Q=mx!QxZI>EG8liU_&vFD054cxvfswOW-H%&WIl zKc+Nd56^lXdobdaQE5f<10c=U#qN#Sq9xa{tZt5hr8U>t3iM~ z5a-B5lbp|LIR(`6sBHQT$=+b%L5dBcMkom+L|`4E40RB3-wZ;CpI|i3E^}h zlht}7TK51PeGT&}wQ$g7HSYA#yHGLCHHxw2^oJ+zyRQlwUQA7L7*gt`XOHk*x67-; zJ{*GQ?I}#V1@5yb#!IkrVB-1BpA{S9=;wboylUaEjl36pR0t3M%vD?(wjPc=zqfL0 z)~=kSu54w4f%!3S$=siN}yyk+@$*nP6`#svc&!5+LKCUt=1_>;qL6g!GKi)3!RR zRYCyZ3Zf|&QbiglG8I#f5J~`ua7EgQ@V1iA;|=ClwWTG_Q;hB_Czh2|o@tWC!uwI5 zSqGErXvtC$Z*!-;p15q$heq|E_Tel#?9J#7!5!{w>MX*>Yc zwzV~1DX21O)(4+C^P8;iG)i$Tp0Y|9>#@AW0ahz=#ZH+gOiS7OYSXxR2ooblq`ZcL zC{Nnwv_pzCsV!IBaAu#kWiHKa;p3$}C`z;1kapzaWBhxq*Gfjgy5x3Xd%K=>p}e&> ztl>(@uo84S{{5{_&e#5bZincv+u1Bt>?Mp!B2RPahy6Y|`Cd{0r5d(`AT_xvAioXH z`9`7eYn5(Ng3MMB+mu?sSGMq-d-Jo~vZMpmbWCySd5z0z$j3r?Bz_C1=I?wrPI?;i zTb4S3rs~0G>##)u-sP*HRVFdu)DLzpc7w^=9eGUT!>`f0w#IxG6lwTEuTeJ<=6b7p zQM9ouP|7M%no~Zkc6dvB6FX&YZ&%jVEBu*+KBJSNhi?6-F=UafPGVcK2nwlZ!_u55 z4=x{C;TKiCy$y74s>r%{!B*+Ute+?JIC~tCALDxPtXn&3QXKoY+I5OQ20Z9jTpe1a zgQu_VHuJJOt|hVLi_rzp<`gp-rR$bR4KBwE9LujtdM9Q6sS3U-o2mR{ld}@Jd4)!q zDI(r$U_!=n>`tW;HU+eBs8pqx+JAKTVWx7eY_UEf5o~1PjPGI^(yKr|cL;=aaOLln zQjjue`=&dWuITUAN?KvvY45qC=(U$Kb~o$45ekVCu##hx>Ktj`!^)ed2&NJ?6>Y1i04?2BKuoIg=p9vr6M;Sm zzRCrsjJ^Lp>-+c42HLFfI^ntuMd8Xr$efo{mUKy0Db@g<1qX7!&rbGgWk(I@$!7xg zK$(Zru0z(AvnOK}{5+8BXC@Lz#2ag)K%NA-)8nWRqBge4(-$uN#R2TUf)7^7ceS!- zdWO)eq#+Z6F#)nDRqz{`N6pX4Hm4gt7RH#K5aR~TK1$MmN-^$X>kta}bHEqJokXVW za6W0)Rl+G#>R9L9|dCS=a9Iwxm<(#d^7khX3Tp)9v+YWrsG!o?y5{IaEbu z(bsd3(cawz+^;T|X-S7O9FdC$l4d8mc|*i;B{k*(Kf__vb)yBE?&Jo7D@-CU-imhy z#1C~(Ih#btUX=+%q8X@p4^fsO=4U18t7-24NCYk=V1-mHqwT(q&Gg5M{c`WoJvrSZ z(Z0-%L)u{@&uGD?Yn}6UbB4Q@V-^Yt@YrDh+O|+!m=p^2F~#Y9*38c_&Qj#&6QlPL z?$}#74Da?H%onI5rh(hrI8`o;C`ccuZ-%fQtgDsA8 z?39BbC)11H&PbkHAf8XbpQ49Y%2&U;D4YYqy?~`O?UT(-E9b~I zXQCDt@Y}9Ul!E*7UC002UyZe2`y|r%>!F1i0Zenq`vcu!YGs5~^ka#r{4BP2#o-?r z+d_u=@Zv=0q7&@5MhWL$-jnvOB9E8g|hBJ6mwO=f@jY%i7vx51D{7OVP2`7)>IV zaJUyQRC{6wc7vBKM;7?J-O_@V-g3p;poRaumhtiqM$YybcMnc`%%|8*P_X58=G#xx z5s?s*KsvePz}$eml_Iq zDKgvP{7}x@%$q;_i<@3(H};53Cz$HGGuq9;ml+h+=j#_`#cgexXLtJ*f4lJDK}M%3 zUnMy~3m4iWYZD#g(+Mwq6&1Qe?^V4M$%?gK>HRhPZlqts?n*nNbpGZSj!^;@KL@Dq5-o2KG#N~Ev5^r(33;Lw0ZnGty9JbyfPPrM$=uo)1YsNRH7HePV*?#xD$ z3b8(|o;2>PP|9;0&8${!!svLMi;puX9WEO{M&+yyx(cu_yITufFBt_7YHCp1&AyQK zo>xgJoVtXEf0i^F1D8Nrk&0`BCbNv+&3Jvzu2Vu6>|ug2hKNd=7`f`daT_g zdp|v6zucxR>`ts%k3!7l{mb*LU)w$HyJE+TCiB=Em7);+1gXBk*>|NF(|`FM49a@} zfk$~fS8~>|vNp=gXa0$4ExNC!SzoD4FhwT?C!m)Iq|Pw#5VUeO-tNce`W*VmuoivF zPj$p4&Ok>R!Cki8@GIM4ECehHVeUs?*bIg!KOy3p<&WnArz zlGPcgc)erwFmIFiVSub%Qp4*-&lVcB;X+1i(GhszwT(|VG0Sw)v$3n$QA*-E)Zkib zscIdNz13=c8S1SyP?z!ao_=dc>%Pn!0!~P%yc~8nZ9b6pV0+90)xkEgHTibDI|RuSotZUm*fQ;b045naB`p zAlRtk$O4#!3DFRK>ykQ6ld4|rXxfahY(+@i*QC=~tNLoit&k6cYVFcAX7bxJu|D@| z9(FP&n}OFtp#Kf6WWVqIr^}+~;|RMLren#7+p_~|L^9=v!ft?sc9!HW2SQ|Csp@t1 zF*~M6OQ8kI@jK%#m}QT>`StwWsdp}fmxELJ!&TXYSFUWo8*tlhyKM8GHulz@%#jGR zyS+0-2gth!^4#81l0A@sgf0m3Y#vZYKqAFhaVIUw4}rzO`sRbLG5t zC6nauPriaOr?mm*%lolt=J_#%=hs9awgWk`*?-@UIN9HO|LJstfwj(kuE%*Nyoatn z(Q~g*1u330zJT=XO+S$`rP^SYXiw<5jJ1t$N?dk8Uy#j#^JEN8hZa0LpeJRcWUftp za{n~4vJEaYc3Ic-RUq98ue?yAsoS9NTx z-io!&MPVi1cg9pIz=s;MvTr(d?U5jYYP{_P2`{IZk8b!e#Hzz}PmI)w?0Hrlxm5k#9&wu8?Q9me9Qs0a&}9TtPq|EvBfu8-&; z7mKi4f0@_u2S+;nNf!7;G#P=G_S{I;qcRtKTdOV=m;Z50)YvXFxB-O#e(XH?GOFgz zvV#CZ8P4Pm13a#+qe1o0ZA!l9doa3Tt&eC{t7a5hiJWaUlXpk*2U=?;Zk}A>RBs zLoAmifu<0@+&1ypzWwUb>`HG-4MmIH33cYxo6uR*YVNq=pz>ZkGLOrq74H7b(bsqdkrXALv_i|HMP6-(T=8a{>dbo@Y@z}9nSdIRoo_~U+`dD_> z661pC>E=5*d~i-!p6E#7xvDlgy^c{KAR}<>154dYLTjU$zu3g5cKRQ zvqsl@TAiMn$iXqK#B01lN3s-}T|qxk*`v^NEaOY^osjb-ryK)FEd1nW=wi*68^T%> z0D!R3D#psw4`U+_`5D#!v!Sb~|ye|&Z_!v2KC9M#Qj}}x)=Jzl_8BeooeGIv@t^A++ zWCX(I#zjXqgq!nqI?6z>ko9ZGky%bI@DO;$w=Mg1ARqRG%A{HKS2S(BI~7z*I{ZAD z7q$v}?;)D9yKV#}Af)&jx7|;Y`BbBx$s=y&ajE+>xcb!p=O3aU4b=L>VoAMr zLbW_ix5ui08$r(c`dUa6^v>iiE?5C@|6ElTWBhhFa!ae&^?@Ksisj=I0fi5YCyUp9 z(8(2K)7rV3%Gc`WFADH_jcpdLcVw8b{~Dr?RPAa+Z&M9o{nXo5UBq5}x_Pv49~$&? zF)QIDLBWSvr!jMQA2igbr8T>;B5l3o`J)RD(9kP_FpR=8)3f^JfA6@#kMQa~B8z-O$)7iN+Tfkqck-GeOUjrF^%tR@)9XgdX?O zr;P0s0@r&1%L^}86--dSlPlko#b>X>3%;w7L^kI${7uMaHEdO+@p#JjA}pQAWx+uB z9tvu|08@VUewYxR?LKKm`gC;)cY5v5Y6P~Who84)d1%r16>p3Y0-;OWbBLVLr)n3!Pk7A^}VMhZUAie89Ea^>ne_1 zZx#Fhp|r-Ycmi3MOGnG-T(RzbbGETV(ZKha(F@- z6KoKwimEfD1HRlGygcInV}*O6z+q26OQFNnb`^ry#PchM9u4dirvu9~)wJ#~_#GSgCRyvQ5>~Un>?O@iK8=u8LL22*UTIz&d zNVnIMKsey3dv-P#v?*whK}Y>CH|yE*F9zh*O|1?kjeevfl33c;CYGDWUcqG>_uc1O zP==E01@FRyv5pCQW*c?>xL}KKp1+y%>hapKS3ClA|A+2nw_s&E5%G%L8nF68pCyZ^ zatb#>bfeS1CyruP&0~v4c*M2cM+nenbO`YuyQGxN4IS1~iLYE>u8MB08vZ5x@prF} zwS+xSM(;#(MVEJOeOZY%_uVJFC>4E8F*kbzi`0csM0a{bol0%!itP{&FLw0srEsGj zhCo%Q<6o1#EDUCyIfcJ&XhGu9vsre6gDor*O)Yk%t!21u*;0UTS!N|0FB%P=JG}HX zEM%ha4l9B7sGgUkfDp1lUQ)W?V9dNJgRpnPfJgysmrr|V*L(j55)Js*V+Hs8oJf+B zZO+3MN8bC0@xKeW+2+uLo-}W6*D`Hl zj##HadY*)8gmvprTPoY0HYT1AoV{VzG=<`XO|P%(5aF}VZcR7^B4Pw5KP?9 zh!t=$gMf+#iSY@*esf;Dc+?F1p#`b6lb8a{r6Z z_k%pFsvj9UNkrS83^a*f1L8MkHg3B$Qj|n8)yv$kPQTt*kG>LhI>|**OG?m4HV=UR zh1q~~|DtSfX^Sx8My2FlH|Lq&R32*9zg5217~2y%a0pk6a;r?Q_g^3_Kwc~h?!#sx zToH(qr_D88n3WocmMbUa4YK1!ll$mQ#%q7_tGPP)_?Qv?Xkh#nJW~Z&kyGZB;KmtR z>G@r$xtopGG?p+W3nja=&ve}h;4L->2D zAJc`caM{;-X=kxw4IhyMbs91bs}F;}WPkMyU<#!bkLB-d+WYYOMk)nR=0m4Zn&1`` z@u#r`Q+oXdn@~Kg5H44cJ>!GN3&1AdtMNe-?9^96xz{&yqvK&@G;P%?JkFEvoA&#t zgwN%Z{(r7IOX~k(wFpw!pV~8g_j!MCVOHXOC;fmiN?$dqD07FvI(0L>LIW1yBlOe( zIO(TPw%Awc)qh!%IUqG6GJAfoiD05}XxFSwpU@D%5(h~*JTti? ztyMMD!PSo^{*l?*On>4&C;CQM>prz!CITOYj?%mD`#d$FKa(z9Uf z-34TYR8;Z~1e#hvE4v&SQ{GFsRM&SIyzr`a{7}vnIOYIlJ(zF3sf9lZgA7hpeRs4T z*YW2(_gdj&UG;>{y`4j<4y!t^FY9YBV1l@(zZN+nX8c$4mIR`%q@XT{#R@aF&F*@7 z&J@GkFlSY1PR7x%qr0;9=Le5#eU~9oBl9nGey8gC&>D`EU%c0|YoN;~C{w$yB@?{u zN&dwo6saNu<)S-7UP&|w#Z79uIT4)K6TW)xnN~WlvKhmVs|4rI7k-eHCZJhjhv$!T zeP>4O70eoynA(w`6ps8nw1_>?Qtv(kS#w+c+EvGcRuO?xW;hx?l`#Tc;V&&qRjf4J zD_jLawlx@3XcRmzklHW1WjJlLi3?qY6y@O)EPNaNE5)F`i<*qSKGL4iR}IRYlNS2_Tz-76t2KYWBzvE zcd8V@wAu}SX3vwh8-US4%`UON52Qqad+H{6%zxB{_!DQ2hk+x?g{T8#zdTPQ3zN>} zw80we56+6>=EDQCkErMqh(f9Bwwp|(D8z1zGq3Hnb?x4c3z+Wt@9J~MnAY!oDE7`9 zNJSxC#D2v&b5xV_t4hTuZFM=5s;l6hmm37ecd3?hPYYjgx~;2&8F@hrGG9oFyDGvO zMs54Hk%x#x%^L~87&9vknTu(bWMC@3uS?q2C#TERhw=AS`KHC@6?s0*kdf>vUL7@NsQ$X- z74A=FYRHg`2>X7Qv0pOo<1GT{7R2PoW~IlSNU3bBgh!omZMTeJ6v>%zZg>r zN(Rktm$jUh#~ph;WVudnE1~yvN|w&9pQl7$kkRx;`Ct6<4_5OtzF!ppONyX>#~zRO zT!^%`6YOjx6h-pU;^H1w8HHi*dLCot`AyKPe4nT&{F=kVmJe=HE7{DQ-{*Wka9J2u zGPSkftNnV1&Q^0=XxE2a)NS*}WI)Gge>LBejY~S@NV_rV&fp`-DEirmerq=U9BWTj zZGb0lv77=ME3m3|*rcBgUFchS@NtIW6y8iML8e$p?TI2Z3iNTs1V= zuPeM>4!sEAXPGmmiozyK#~Dk$F?7Yq2cqRsHnTW20qx zCe=tSuf#Rar(C_S9`so*G@!@6nR$gg$ymeD(QyB%;JJNA>sk%jLoiBFU^)Om+p@u^kj`OdOiLaSPo44ZETx=111@%*B}t@JHH&ULY+Mk@7zW_>Ws84?C`Ld~SP~ z3|(^r)5i&VGt*?^znt*#ak0^d&ELIZ6^7^kS#BJqYRnNedfi-`GmlV)^MeguDTeZ# z2)N*0pQrX1Kcz}Mn~CnogA62rcC_%To7&6shI$2(v+F-;<2A7V%lPe<{D(Ej|83V9 z7vb@|_0{Lk(;#p$$SQcZ+<1b&penwwbqi2 z-FV7{$Tr!l7V)!p!|YZ`Xd_zMP+K^7cX%e`gaJe7(Q(OTJ*c9q@N}qWnP$yn)r=Xi z`I>EB?j+fwJv*w4eW?0gd10$=Wtr!VkZK zI9&z#$M3)}Q^XB#g6@?l#4eWFde3AN-GsY}*~FNN>4Hju$1zdJu0L2!+AA_hvbXt= z@%TiXKHFqDDBZgCnq;0S`Q%?tj16x?UoSM+xK+GlU-2bjV(YyL%oPsczG%v9 zHaWTGTiV}zV`$WX&($876-L4hSYFuZn!J>jwhp!N&(+F)?e-hnDBaj@ zZ|)u9?VU|epGHeqkyMWkbWT^1rH%YoFiM8}Ukj=*^h9pLVn_E)9hRal4~h~&E*-R- zB{B=LsrOg0xCFPI031x}tgCopNcbYJM{PNeT#Ym+R1Ab-Og<0Sz1*cctz$0|iEqpL zXRob3c~)%+bVK-_+bbmac5z}uI9k&6Eppur^!X$iXQEY6&)Q-rL{qg@QI=gvN}d@% zgvXM}%e&x`E;7@d4h7i^2~sX#*qJt%g&IUXhx_@zExM~@MgqMM*o09&vktYR?oo^o z`ojr(R4$Jr%;GIh*8$q+I=rF}Uuunn6ZT0+xw8tLC9c8oa5KTKM+>&%)7+ zn@Fa1=pp}C`SSxpk14a_%t@JAk^GTRq&uNGywj6J7KN4{m+D*92iw^|D-#)$5=*|5 zFl6SNw=HM#OTtrlFv<6%<~r^dQ;(}BM?u{f_LXx>8{o%&K2Tqg`s7Xym(o_<$i;p` zC6);k3p6wVc%r^0e<)#u_oQyt*Z(OIcP7`DF9rOEGF1tDr%E!AiR4>CYJHfNsTEdqy6(WF>#7A3F z=_hrM*uqkW_dIU?bnE*3X=n(sS4129aSc(7oJZV=)-hR6+eCmMhSq}SqM|12){Rp= z<{F5aDoOwT;tJOXFowF2ow=h!e_f&(eQgCz_QY=GjjO|Gy;=+rSD>~kiXm?@rCv{v zQ#uY7{u*&;DO(#+S7zTj=~%o#G5@_mv13)C&zSCF6$%9)`)FJ@=V_UYZ=|-ovHHf# zR23(x%yFe$Jz{QbOqsP{BB+9>_pch_hBHOa%|}E1j-=|$%YuNB8)4BdRGFv6`qc%$ z&4zW9U{{RT&qegvmNRQYUOTCL+6yKTc)nP6U`t*PJx6IydpTlM6{%ukE${=FWVJre z?d*dqjfOW1ThApmnvrK>C>!%mEI#Q{7KNi>XY78pPL(sUs9zQ+EMI+W;A+orJAHZd z2?M2ScK!#^h9`%|c)IrOkX+UZIwQ{KwDmqF7vOd${E^1Ly#OtT5mUgNzoIg8p8{=? zJ}yHw+9SA8Lj|pJ;pE#KZ_G^H;GsGTGZmIYVt6qo?nW;kPp1RMiEwmH z?kLqA?Dvj3rkSa>N%D!#oNxiJO5fLi*bNZ~X+Qq)Ckv$ZEY9a8F?+Qs2_`&Jw+5iB z#%#X2{SG4=N>HFfkuiI`R4O;t!ziXxpdpDi6OLao`RpXI{mBt+@u|NkWr#?k>vzca z@Nx68(z}!tO%+vx@T)H0^oiWGXzmKzj7O~A zsD?0{yol){doSi|npwEe(&@?CRqh3+2f(4notV!5)#_z<+}3g`bDYvBAhqetbUT@C z#&6e-R!QY|jg{w4D61&Y23Xn-<}5cSyLg;_j%-DT7?dgXh6Yt$FE!Y3h0UOgLZbD# zOh`LkQT%VS4VFD9xmK6j1ft0a_7k{R_Yg1`hVmUzV^{7m7#mZ{@>cu4mw}z;%WC$J zhV4VPA;juSWMI7^ga_vR5M{EVZAv)Brl{}76Yu^Xv6-XLQaA)xG*=mfzh}qBCfc45 z_c^}gXs;Pa6alOsU!ZXJ?=JYQ;&R+AlTVcl)7= z|C;ON>9ZSifIa?1_s1cnh+xH;CVwW{xe2p);MV#<%$8$8G%9 zI`39s8a4Mebxk*no_E=M2n$Q>7$eVLg$p87fa=7amB23Q#W-O5QlKsSPPwA@P-s}q;0(Ld5sDWv``2> zSS;#R0Kz%^k6pv!DGofEF(wlyEs57vzDwzR@6jF4@2PY&Kmxnn#Oq7Mueq5wP!!9| ztEuA9oFGED!?e3HIB>B{#zSldqUC#+#LULvWjt>E(j4yBCg`L*m|%=Y^;8cpSP@1k zgI=F`FiIDK_^2_73;bd_Z_8t`+JiZ>WQqe+;;3sw$0qVX^1ZTL*c&rk`nCSP>-G!U z3Vs}d{Z0PP-iGn>uKb$+4MJB7zVw7ZdSiTC!{q_kMOobhKh0zvx4zPr6~9BK(ZIyW=+w+4+r?>@sO+V zR#8|j=eO^Bu-)m6vNc`<^>0L5FKg4YXNu!@LU7}^>r8AmJRLan(9N^cuXF()aV>NI z+LVz^T#no3bwkQto&I*h1WGF z|8@{X?rXIjl<_$Ss5_Q<6JDopicVg)pbz#Bb5(qi(z?J7uO}aWzI%X+Qfx_SX>27>6EVY_C0R&`5 zgOB@dM0o5Fh|PkxMg!L04TjJiavID5I-CGq^- zlK}ja0`9AI+0JeD6DzV}rm}zh!yq%mUlFK48?A}Thew^LkaVcHbLgo7E(FeSqgMT^ zI=glQ;WDO#nqoV@Qhlv^wCJ_UwR2OV{sIrS_U?C-I&@Jn-#E;0h$SBmJl}kBhn*-! zWNN)+0lS2TR)j#4TIc$$a7On33L#SOyw_l`qEvd0N;UA-(zl9GMoiF1aWTNQh9`|W zNz8zBNk7^URYFi62*jGP7N+#ZzyzJT8=X9_>=3R#n$5^<>!1xm4*Q2VOto1u3lM_b z$@W=16TQ%kiYhCkT?Lb|c8X?@7Zx;foOj4bWMaSMDw)MtJk8hJt*s1^p6Eq}tbeNF z5<}XCfkiy+{7m9WcA5(jd3+Xu4R5m&I0x)La1EFnRTuE(Zr3qRFs%w}x}I+PlkF%v z3%tyMZ|YUEwY$qxCx4Z?uP~Q}7oLRDJ!#4IAe_e1eEIK$z2E;EA++&6fBtTn?vaQR z)C^1Ygm$~677g5+#varH%Us?X)fv<&b7p?YmHQYc`BKw#8{qehP~Yq692($%So$-@ z%8`eRqdM_3Cb4qDI~->N-InI-PCfYe69#GhqG=G~d5W6#nFrJ!2sjwi^5^}H$~^$# zh!eFX8S0rlj$Kt*>75b#Nn9ni1k~tScVHqp-Spt_3s>3l!lso{u&R?YnfOEV5#MG? zg?u50Z*hCkf%Ehx>8(bkHEG8#|~(};n-Ll{GZz6-ylM# zpApvmPJ8w#{5naQg$M$ngW6J|$wk5>)R~um%^QA-141#yc`k*(_xJ7}<0PLn&X>3N zVe{)-H+{zf3tG#iZ_HToIE+~|N}NiSNd}uDVFHfM)L#hvMrj8cNhG{3`(%UZ*&t0JtqDXQMciX~`W!V4H`oBfHx?)upgLnN|} zXQ`zZwK#g}^TIk9^}&N#J>{Syg|L+s+c;5o0xXg1_f%67fMGUhBLT!~z>Gsl@T#q# zW9q}4%WHY72EGNE%K%;&Sfjyt-ICYAzDy{Qnw*<{TCT7Oo_$sZ!fR1QX8MPOh)rFlF5k;71WID0ehcXy8@*^;Bf^TO)J=*2kJ zmClZ%q!6g9>xe5#5##R-N)~@C zs6`QHt>YTPMh%5t(j2I(srBuE(`FaeV>v#6G5)#k`eCxc5xQ}z|8c=;$+f@x98ABn$Qo`TvQ5OfW8cFWEkwIfN&4L)wAEwXc-Jv+l{nG3!qW5oQXwW;b79&vfHb}j@KG<{RygUL_#h}v zG%6H(r@*WM59EuL*o^k@Ap1{8?Z3MoiYhIR=gh`b@N#neul0!E9C0jqMCMSP}hF_5aG@y427DqcHv=e_N~h_24{k*o|DVU(>k?^}H-M+tAU{PdU|6QMt$a*f}ZE!B+(huqgZ zOY);nj(26KedL+lXYTn&=31@6ii#*7QA3le+al{&KYxwA9+lhheEzC1_k{?ZmJ+;D zxhVe~W|L2gien)s*+yA0; za-6SuQ?SKzr2T?bpFXe@wJ0&4uh=|TbFsBKpAoskbjQUA-$h1s#UXI9$#O+VvGmP!liN_;IVI53kD1%;TBNB;oJwn!B6}NPNc#{U*k2S?~7jVCebC!)v!DuAzaU zW#Esp?$WuHjTz{0|L^{*E_jf|9-lDy6wBoOi9(M zb9wN`B*eps9`t-{Zns-$h&7QwN={7_li(lked#|H^+X2QTwlh6$UomPQFk~^U=q0cighayLRV4SHKp=#F?qA7QnJV0vNCvl-{=~CnJ&wM5 zMnqJ}jq1KI7Am737{}XGp?_ko{B3TD2(d&~9*suN1Q*THwL9R2hy=aAps6eADU5xP zrsVxQe>~K&24p@dM}u%HsPwx_@PP+@p6o z+HtM#&N&cefX08I0H2e&wShvIItz{L_XjeV;G1C?R-$x(?jB(n&Gqi z`%T1Yb>j9gG1@{$MnloFX@wQ1s9BmAb=Vz6hDTBAIpW+Xn)g#e-W==JiRa`uM;<6f zaK&!~PUZiT@a&-ftC!4?7V963)rukqjF&W{^?XvA;mzq|fqpRjNs3`+-48fB`@|0* zwzjFZv5VNTRn*l*?zg5)pnCR`{bVir=|~c-25s)m-_$D5ab#s#=Xt7=_Z~R zP1YWHiwV{)^Whgqc9Ns-!gq=XY)T^IN1NK`hA#)?5IW4{D$zNlK&W z<5tuM+cz7m9`n`;PU`*n%)*n3HYpm2uuKnt0|IR%__9)+HGJVlgv0XVdI#hP)QSXd&5GI_XckQlMv6^$ zEo-gz{Di0?!y@R&P_?Jvv+v@7Iip zL>dS9{ohxF?4*{v?>SiTAQLc3*(03mFLr^LckOl%%^Ve3<+ zYSgf|&>UGEfI(g@k!e|ZzgBZ^KN7V(zrZ_#7dJ?W1a`YBy;H9aY9H|GHv4T}aA3db zpDh_`FlMhS0uvMO8E9{EFu*6Wk5bhlrJXh3?!X+)4^nT!0D!4e*tVKQDm|x~+)?=E z)eLmRf0c_YKdm|2g{if%GSdhBcG2&DW&0&d=|T6f?#A&|X~Bq$F}t2`bGRC}BOi9B zriR3E6gG7e`V5-z%ZKwrT$G+8bUBdp2iNo4!>Gi*S-8OLH+La~gBN3W?V|MRS6*G|YxI&cT( zygo9pYl(+BF3!rdK@w>OVm|3~hdXV{obPN5=N;^b`X<&+tVrC3V8WK#5HGHwMk0r* zcD$l6VhZm@)BtIqv>{BV2i8A)M9`CZ(QV|ib`~&9(~=FQSbu$swWC)50*B6_5GP<$ zDm`bZ9Z@z!(E2Zu%U~8X-|rtYEr&K+Py|L&%E%ng=+Y2w99E^ap6%eqL1windt1oV z)Ppm6(iZHiE1z}4Tx3v>m3GBRCm=SfhNoXGb$GWO@tABO9VF8Q3QgoxE%%#;)}Kcy z?VPIi&4#E=$e6-JGsM|xFakb>*gSnB{`I6tfWLJ5`%1S)IrfLN|82B>UUwWJeA9GP zX+N8~90EPOJ7R`Ea55?%v8CWD$k12(VX_i;5;3^ z#p(&G2c_WSI7@{{4QFexd)Dz(6^F->ofqc6@U`!oveqJHi-c4Y*Ow07Ezz(jk@&bD z>iRzIP>3852TasNh9E{fg_GiyH~rz5^mkPg_hFoaL0D!^ZWUfa2QRXXhv z;C$bN%4ZL^g)D0WhCwMrvbE+kFXfk?pqi1bolFP>qVg;Dg-F!%*o~k@dvLZLzDwv~ z^et{iILsrzJ6zT5d@E7sMyLJM=A6RX(+Cbu&+s}67QTDJ(X7_~3kY}>A z(){z0I({+Q^^2}oKX+&`OU15h_}kU1CNj$Sv;zRSiZ{f`EPmASSc@Avi*%%Z7>+HCyS1U!kFaD+2GF7Ss_KmL*7Sr49WB z@Thkl#Ha9IAfmP)B9D-g(UZ0_~mLiXllI@2$?w}H;5>jn!vhh2m25A=fhd> z13#`^y@o$-x4PHe2MWkMDmBI02Um6{E1Avyj|%{rj@!s$N~1!uUudEjl+0|@DcVoKJh-a6T1FX*P01@_u$7ist$Uh z&0nG{Jcy~ij0V1@OgXe=irS&LaS%{V3TvdrTQo2GHd*1vomBq;{N6|HC@z^X9CJ`7 zpTUQJ*;~0N@}&Y!3RpB%c%CR!KAx#8wMdu+%QM^J%AsH*Ke6xF583y3Pih{%7Q}J$ zefnu8Bw0i-LyI=-^ox`pi_1gA9R$ zJXx9vVWK=sa`BVLuGcNS!9>wYqyp*J6#Q@xH=SIHQ-QqlIaA#a z)JQnfq??Pq&w^~JDPfQlb7p=9OCwc!BUH=zs*TDOZDpQp<*GP~=>ChIsMNz9 z+!UB!naM$m1>ex}eCkFp&+}oJ8^S8lQlh2mOSv$b3_>Jj0Qa|DH&PCQE}izztbCfFN5CwR7AqOy89_ zSsLS)9%={^W}V-{yG4rMgI9Y_GqGSNe0hy@DE=OnKxe(lZeiz=1P$ngw+s$d_>Etl-yEE~O}4;xlM34()7Z@%u9sY1K0 z5(T1^Xg1y6z-C~5QymiFM9!!{HU8(!@7T530)^>Gl2?`R5;i3e)sL+$iueGFhR<5n z&l+LOql6vOhkRqnR@h}wKveL2D4*%ni|lQMNTn;k9Ei{C8NiiF{uShtKXq$E$nndA zR-`g4=_^3H#rLy?s?`2rTwXt$I-)gs(dMssj;pTT-4MIu=$1xPnD70(ELP7yv*@Hq z8T}y_kuUh-aSQ<>tNMnJ^D*kg^SON3b4UI`{qkh#{no!X5TVfWQ9`!oaU(29^RAo20aV#lpHyT;A;K$oq_@0jDsY@VE zo|io-A6%Jc>h+w5m*-7a-_6BlKkflKjiP;7%|b~A6v4jLm{?Ef~gC7<%d%~gYZsU>Nu!Orw!wDtUuQ_ik_ z(1`E6n=0&+gM`40g>v!dJNo=qRpyd zKDG^Qhs9>ZSuK?iBZi-&BvUdDS>LT6p2zj{We4)5T6CnK5g>UUw^hS!QJAtL_;+v@ zdu98XszuJQ2A@)}8}NFk3)QvtCc|rDLZ8?#3*6f5haid$sU?}@hZAOAD6TO zq)s0v`?;(=Z`%3EuWap`(vc=XcSk~)BM)YlwHfiVmc5yc7mMbxBWn*#j+NLYA)Ob` zxlNm+*$6nk{>aam8yMU%Grz@YY^)qpgrmO@>;I}&zT_5xc>;}rDfEn6PV{MI7+55V zKyhmFoX80Yz7Edf5T-;*65v=~%aAq57d+46N z%hlPMs(RmINiBQjERDySo3Hu40_u&CCD;j1m}H&KX*r{r+51_5KvLqi`_h{gWcp!n zYI3ijM3&ME18k5onD;ovRZA~z3y;olIwImE(Q@+?P8CXIVmp%P?FzVZw?Pk(#(FXj z6Uu4TvWPwS%s2U|?u^CIr#3|UHV zW!7&?2u0h3)ayN)?(Zh$@|QNpYMhX)Xrb~ylJTds9c0P2p_DP_2koN0Rs2Hs)~LWw zzPZK>h6^$~OF(*@I+?n@Os_FhsJZwX-)yprVUZx3Z!e@5qs-Ufck=d~L|Ec!Piffe zGh>z!_YpgWNvtULH}d|+5Pn>1JjEJQ8$|-3<}S{H$oM{@Q~Y;l8iP4yyJ4RJ-bpb; zweLT+$ZeeB(uQrmKIDQYNuzbR=Xq51aB0_w23xxFBSvMJb!U3-nyp*ybz;HGGFA`{ zbbKSS0V$LeL{5HQ=)nr_#f8QaL!j5eD{_vB$nF#A6Y+V zd@<rx8PB&1EoBCNAhX1K%PNjca3d+e9! z9%$@sM|aWH8W&7M^J8A>tzw%o+A3C>%G{Eq*(0wz!IJrbd;cf#?QJ!2zw53|3x;6g z`NkZUY}D_h{7cWewiK_fRQ*6aGJy}K7Ov^gSO7Rrs8{MP=X@<=w z+D*<__-@TVC=7@Dc#w{oHkkNhoIb1bx1HMalGhaU)CBcsZ&)tFen`hsO4l|1Kg!-Q zxU+6)9}Xt=#Gcr8GO_JsV*Fy;wllG9+qP}nwv&I&ocliKsd_)XAG>zd-fQ<-t*frp z{jq8{y3%Aw7MS43%(7tP@@+qdicY zAv`~?s@QBw^Ona?=#F0dsEg_NfQs|0D~7NRf_qgu$|GKhhAWXqPe%>zp@HOFyCY^+ zm(Ig>n)vZ6rwMk4rz zsFs-a2PYc!mav*74NC!++UF-0)~l?6!&7CIsHjt&+X}pdIkrjh>d)Q=#i!10OLw}D&eGjqPFWLG`QNH+UedAMlwVAv?0Q;io z<==U*aAci4^j=NexE%1?b9yB6{teUlitfeXNyPiMsMX$3gto?(Sk?7T?B9lw)<|6r za04@{yEu7!-dVTPk5rCVX0Bh>$$>Rdzcs&nosdwUEUTyhFHL{$jg_f)o&8E&V`p_* zt7*16qO)5Kx%5o=>bTGXmu*SZ;JqQqY`=>e|6ELRagf$Rk8YroJh?w`9z$PP9W-NG z&chC`zY1fDBRt2jFcUNOP{;KYgm1jSQ^hV<$*xCt5nk#Hkr?Uay|r5x1H}+2Yk7`h zFJ)b@4DFr@TUkyNjLW%xlYZyt^~wr8VT$$DDf=?e@UW{d10rLTyfDKV>S@F&PZk_+ zw6#QEl6Kf?)cMg5^MZl_K2z`&+~AEGL}xR<&Sm0zA2UaTUDh5_irlCh9BsAHruUfAlcAHG`CiKCkI`TnfWlng zjwHgmq9)hU5Pzt{#F~Jre7s2$T~@-KzEx<(EB1t>n9OcO&dubHp$;hg#_;H3FUX39 zlWSz={aQ$gbr&?#Mw@le*0-<4oa&27WqQovlR48@UUt5}>xitJK(i$jhEx~Qg?U8O zrorkYY+KfHs-JK$e*%n)H#vMN)2KEPb(g28K6IDL*T z2?akJBp++x{DR!a?pCeM1Y$}1)$|gxCi)_6sm67P;54qQSnIT`EbXeJvBn~4^)&U6 zZLl1J75wnVDXT3(snNyHlq#&C1L%7m_J&=yo%`5Tm!PsRYO5?+FqO{GZltT`OAQ@E zzAm-~YlY!kkJ)M!+%6Fr8+O^m({KevAvI^&m{0Ec1);rvUDNJz@dwb*YbO^( zV#KNSV4xNqQg!5h%p$lUVQN>=q7rdfnIrc`+^RkqLVe^P^2PT#Gs{IvGnpTxM=9Lu zzPi&(1uvZHrbh|Vt1o#pcb^mrOn3HJTA0|$ywG2RZ`{kp&Cuj;I=E?PT|cxX72q*^ z4y|;7H-Bp*?^{}5bjN8o;|VMf?p=p@BTqCIN@=#o*4ZNbHSM-9D21A zfZRnk^qY@#wW*Nh&t8Zm#RbG2-2o|Z_~HDYP53C2Me{jv zUl!@lcSUtn7rN`KX-su>+&tGz-Nz$f_#J_FSZIt-yma#4o#a;HnlIvxoNE0gH!_=2 zJZ(^2k0Jo;FZg?vP;D+p1x?H7Uk8V+R&7PyP!&9mxamYVl@BM(m2+NF69{Z}CNFnC z>ss*pM5MRh%Xr2DdYxSjH;v3zEmjvKmPFe{ips7E{uDg@D+lop%%`ah?-yfBH_?2E zxZ#6+)%+f>H|Wo2VEibw-flKbY#%OA8B)9sLX`1>S)HCWAEpjt*dz1NUSVxUg*E%= z2)82d6%QR%KgVjLJ;Sdyo8olZt3y{KYSwS2$xiAn8MN0t)8iP=B*Mll+@df9t-q|G zmFWt-cIKoc10#oAX@(~Cj>@L$9v7{z_pVTH%Z!s^@!@CA(qY(_v`5FrBxN^ClsN%a zoqpqe*vpQ^=_Z4aA`h}TX6~3Os?iPHG9QGWFIVoayPv_$1?P0*y!j{ICm$XMu~^z~ zr}|N5J6!;dEJR~ST%8#5A>+71MInnP&;iJiA5-+xGa`iHnZ)~?&yy|Ra4q@rQqhmE zo!C$JNQcs%e|DCgnWYy;qCR*pnkv!C{}w83NW#489T@k&~ZX~r`GIsQyBy>rZylh|txOSU(Y@Uat(DsbKqIh@dv zb+q&R%^alTMFkK+&=c;tWW9()V_A53hPWqPTNYjGWnFUIvyANr&y-Hq4U`ZBiLo!=E4>ZxTH7_ag_u8;$ppeQWV1NVIx6KJ}WiG75yuV9D*%j$7~|!-Ha5TGujuEK!OebGW+1pWO;sZerM{8 zzi>6FeapxQ{v^X7$I22@dD6x00a8GESGxBHxBJ1aN=4D5$!0S2LxD~cNxNlg(Dm983G%kPhPx*%Q;*#v9~P zQq9R$^zG1-*W~zKTm0TGT6@3$Q;WyTr|fn35ng^8ugHL}WBccEZk&gav~IVu$4ANwLM(V-MWh;Dm*jbz$CP6+&=_G^qzL9+-YX&#s?&-=^W;sKJ%EL&C6}|- zt~pJg_|k&OP-K!s$ulc%*X<$=p0$oEy7h#xAmjC&o4*FlhwsXUCLjY!KYZ`8aM7-9hlSTe{ z`-moILoA6#cxZBp!&EK#vW|qZ8m8N*iOBxBo_&LK_0WqHPK=kp&D`$B*KkP+a z%&QqXu4%rB*qe5yfg{@ zfYuod7R$>NA(HQ-uX-y19^TVTOR z6kCPto-HP&XA1)aQKUz0wcnuf&6n#w=#h)23}$y)EXbo`XPrUfL=Bu?KU7x)F&BtL z#7)5-ko?DaxWyf@OiY^c>m&~vm2q(FIhe?jYIW@iVsv!|wK-$P{e&%|$-FPJ`b6*2 z=})*W<}{g1c<+qp^2N}0QUAE;;B`~&f*+${-XD>M$d|?nHVAa@0NTJI)LS*`uag3) z=aU(WGShXYV4cq?9Ius`J}cm__i=;Im0R;i5ij?T_{BArjsdo!_Jnt4r)x*A*OMhv z?tn2@4u;+X-ox2_6jAs!ypGTjOBq~^cTn1v+&De1HP?H9<&%k|V%LWuUdxvu-pRIn z(*j1S+C^t&iPwWyVf(5X(7DTd;voZD`v|DwW`wtX-Q&nx^T9~e;QaCc0CydtC_UlJ z<#)o@vB_KGk+kM7o~d>2K;zvkCodj?NehXSL!r}e`{EUpZ{_qc2UEMO@kq_r@Yi57 zDkz2+Hyp=N6|GeV9O`DM?<9C<^_Fb55r;!*UmKkFiZrFS>Qk7*Sf}0*hBya+N|ZP zLQPN;ZyGODzgmwtNxkpGrK+FmbQh`4i#LWfs>}+dT7H*ZHOHdNk!m_7*>IqC2K2CEcQm;rn25MeF84QSfI8)%J3MK5<^mHZ1!{m)&_gjLXS47GAReHQN6Yz87^ZLVHeXKX0ARNIFd z%V9WI_+waYu+U{MWZZ!C9}4!Z-6Bce@@z=Id77Lyb@Zf_s50z@P1$%&7dGk=oi*YW zPE}7nisxJfP^Bz#$7Soy{Ysgdblx;__bq0kM^z zMur88=gnfh7-ZR<62XtM6R6?yosF_x>|(nB_mnsT_U}bSJIvG!F=#-T*=>!lTyV5J zE5botdJ+{jx$QOSJ;}WhkG8Q0$Flk=C@tV$l+X#X%oz4m!>71bHy6Unv-NEqN$)7_ zkd-XEI2kfgg?(HW`{h}+eGmX2h=1> zes{4CsO8^G@`C(+8+Dew`@;r&Z(44$UXr(TPNn)YewX-rp~z6bon}?-)BsGWIn%!~6r7 zq`%kvVT)p}$s{Jpa`CpF|>KshFw2L~+dp*?dbL{XzF zN*HmAsmn#YrV&kfeg_}G_|e=z!c5}=;{E9cg6;uJev&haa)yCZ2dgIoLt zCoDX_t=n%NJzsR@6%r>>0C(hfr$D~u91<3U(Rps4%;}2p=(}Xn)LnBNPiIXS*Q#9i zBuC*WFFfc&A%(wFWOD-?d|Qn5o>GXgEUfRbV%CuV(EE?eJz-XdhNa$IlZ6?jW*SYp7Qgv_{t4}UG8=1f@uWUcH})YcV{4m3ZcIgX}>@1mCg;>ROAZ-whZ zGAW+Bp!{NE9v)imh6fmWdj^;8XLDc_iPkITqikkft!ba8Yq35vH_6DAXw`JJT++qyoDS&46{+S%KtIOU^b+7@|b_nr&`EQe^ z5`sT%!_)awHRCu?xtr@$F*g)ZqRzn7kk$-^rNQsYYE2wGdD$WD@z3P>yHLWb z54PF0LyX9UOSgXPnuHMozXs_q!~dd9zCl~uPj^H_c!!lEwbBBciL3%#BuB5^(b(bDxkf0vgetOH=)~%2)FIisGv0hJfap2z&ugdFv*!c=eiv`I9^h5CQe%&4pj{mjo;KK z+<&3K-8#ry4;luY9GD9L4>*NLJDLXI{^_Chmp_FM|M$dV63Snw;K1EF*jf)7Gshm( znc7G>u@uVTz#!U3x(KJM4*Yp)VM|wNu*5`I;OjsXl+*15_ZT2pUNL6itr&8H`JkDq zD4mLr1%NNiS*804ti5ea9*+b>7Nk`TR-_9PFw0aBkgozMhFAPfM#F;Zp0c}T83JyF_zfmT zP|u_d<&~3(f_tm*8ZSViuz=>j+I!pT14a(#drBY_m zoQV-2ZqJ#$A<4*Xivcyq;YG`78xZ#FMyqSe-|J zszDV?pm(c{^TGY|Koc7lKhpX0#vxh=lA!;_=3Ny+nPqsaJYw)U#nscca89(Q#L2qIt6;~(wl_D0+0kenwn|%kyV+O z@_+c&7sF*R1Od-1CT%F^F z{SS$>U+r%@iD$a`^~Ew8ZtL;?ma;)dP|! zw5t_mQSF>XDjXqnm{&y-_K!ASB!WoUo9n zzbDM^@EMj5U_RhmhzOA?@En2T_6mT76&`^RIo^qmwOY=9Zf{hFYpz5`S+&xahnt3u zml^MXfTkN}N??P?4x=kavSnszj2S@oB{?MNXoZ((zUDU4fH%wdWk60xQix8ieQH4p(R)`b5`f0EC+znc3SEB@5BYJ7k03jSA~1o^Xv|CMdge*YN&^sgLJ z_xIl2f8}(rKbhZu=|w18 zTC~5iLSJ$Zh@KeVH}>qfpq8|+p4}awLDD8)V)voyDJ!ggJZBtBHzUouadr zI>_T^ukP?odwP3bY9JWw2aMfvM{2gn+{|exwDcgXv9j14u|hyxKgsNLNPbXjXbxEm znzr+fi2mSNbWU2h>DC))KvsCZ{ekZpUsybNcrYPdex5K`&OmCEc4j_`zywGd8B+y6 zsp@uQS$KfIx{SDP^Mv1_z4Tw<3Vfo4o7QUJDV)RDtFhzN!OfQgs+{Pv$8zO?J>dZE zWaa5r=9aRHFKk{t9FUg&skr)>+4A$r3$%;!?sBEL=42eNWS(3E_G{KnoCbRB^u3|gSKk8Q%U~CbG|(Wcioj#aPyE^!8R+wP!gRYMIy-U zAqCI(^Rdq|bw#dJMu?QoFNUt{VnZt2nMpW$zRPIKL?IQ%P0+?_|h2zBPh)%Mw?vB+BY zgrf^UF(6o#)o$&b+&z9IT&KKO-1Tdt|A->3-;v{nkFvTtsz&%@c2sA;O?NVPyt z9pfOTLHgY;TKXHi2m2`9BJ0iBhYaI!8FlFG8H*lIQx9PwtcWvHdsw^7rN5%_%}|5% z&-NbuEEp&02%Rd+UGy{>rv-KDl9_BR6?AqbQ+8{Y-xT~4sSj`-`r^pE>YKwEV}N)} z(kzS&#s(g(rt|%Vitzrg_fM6@Q8q>rpcYw$s zb(!&fx6le&(cgW4BUUP_2W&&yLtPPTQF~#BVKW$hz+Z6A*5%B`9#v1eW}p@$--!t` z^Vy1Wxsz)2``GqtjjsV8<^RliQk#tBSn}e)*bIm@X?b30wLFS~cK9xCZUtUl5gDui z#gxVf!J*7zu?1Yw9s9ZNT{Hc#w!WXcG@II_xxIQIxBqDGF3%%>#?eN#y($#KnX~wk z-^*UC#_wVR3*EC2d6Tz|ePAfKJ*BMI1k=`q zXNbV+%Sx;mMlx$H5|z7`d3Zz~9vG@e^ljlpQSULKu_SWebxI1Swh@~OX3Rc2o$ANl zAK)~amDoyKdv^;I(1&bOn&x(^c_!bcZ@P=?+7X$X4!nX0*?_F1 z6t`~2Od78uZe^5gSCWB8~@~^E;)3>+^K+pM+D!!%OJK$t4TgVq`z{@Bq>D$|Q{2t**Rb}JqS2v0e(%0n0 z??B0Sykns0)6)Z--3X25{Zzx)-uVeiq`hYJO^WvHAkawA?F~cQzLtCW$8U(>UmhJdVoyD0%ArZW6HkoY8bJ(gKO9Lqt;V7g5MgmHlpz4hBgNQi1^~b zO$>w1hinXGN^@<_KG?TX#Wl};y-`<~K#3EIYnHu3^j`lOfB@vpPTn7IPq7J#;L{uy z*X{BI&+p#|Y%BG6vF50>mNBpF9Ed33U@@8h{ZNYXUxeUKXppHg_; zA2U);4cq=(#IFcb{+U`S$e#ntTf*VVZoI4Pak|#E+qBgj@$>5hPZdH5+7tXJ21zRC z==Y}q{*-ma2lh-n6+>IN?;9Km#PLeL$JXla&lHUcQjI*z54#lQx`yJ!D zA$F%WkA|;1_Ez`uw$7ce^9I2C80S#xcv0+Rpo?3(?C_TqeagQdxVrADQek)qN4V=2 z_DcV#dl_`r$?1l(y>b4W1VzU@_xybOsqIRU)dt|~vqy0ZW~0AqF?Mk@r%^AU-w$(d zzKWY0mdgg;n7q%m|a{#1SV!O?y{=~Rjf zBs_K2iUnXW?W@|3TC39Cz3$x4C(m^>+_%=5>Sfd3``~^MYpVH=+IIW?4#s`1o=KqA zX^)(W$P%M>palfKsa2DL`x30*cHrOM*em={AMxc5EZ!9S;&p$*#T9;FwRz{NBk556 zE+!@bo%6I=mMtb9Mi>&3s|zaB;|J#6=ciRYD-R4tK?pr-4w6Gc{2Oo9z1DD(_xq3d zfJe}Pr8`qf?R&f&5TFPHG5@&U>U_P|H^@-Rl;DH!YfR4U8zBeqiNh%nXQ}a^jkM7C<1vngFxf>HF^r`w%SwU-DXwoQD z6fI)D!tV}mOzr!}ejO_jxoq1=>zVFZ4?=05-@8a%5of3oA;3`hsHR9rNDz>XW?`s( z{M_4a+8+(b+8v=&je7JQyYsA=tlYDaCIor*TUORaW+u{_)hyNKe)J!4%Ct0@=C?J!KE%I$L^G|Cdg63#;#2U<& zbL$Pz-}+r+%{yetgWFQ}#_04JE6b;4?Ew8trhIq87!A=_q57dzG@Bqs3U1zqXMJf5 z!C(JizbotR)cEVSzHL7p^|!Ts;&SENoI7(q|MkA|7d>uwWJYn`%;+F#I1bfmj>MX? z4=9gL7WCl3Cq{reFNo#YOqJ}nE>B0F1nVxb%Y6N{b8Dnfb(25AH0J`;3SkUm#o;gJL;^(P7+5O8b0+z;U$XJAmnr}guedloYBPYz1}|bqN_6;*o^TqcO z-Zy0OxXXV!orI*tfBWP83iGy=#t@9brX@zksf84AZ0zP$1GdjWO&7SY+ht3@i8!hI zzi#oaP6lnfzl@%rS#y_$gYI!+#|K;RuTNf4aBy%bXKN0BC)mdnCy@jKh9v2zp49b# zg^OYrpogcb(sia6@wiZn^={w?W}rmDx~06dZ&C;v4PEyWfLJM`WZ;zrW6;O*RH`HC zbg~3j{GDew`UAI}oEbYUy@Lj*kMf?-krK#e{wXH=CqzL!1t4wfW|j)wqeH@8?!xq! z(apgt;&b(3u)xd3yXY-%{Z3WeSPZKHpBm@d^1)~uZww}U=mJ{ibEfCy`j71+nY*!J zK&hu;ZDk-YEMOhX^mv7I7;83U|6md9-5n1^M0$w zsA8jQo&%Mm9A>or9t^xKmd9udTA8xa)iZE^g~u4Sq%qrB)cp|+yE@cwjj(`Xjq;yt zEaZ#$LByC3lAwQpyqFWUBNCY>=K_QE0W)ZUDc6KF>Z>OG0t`!TFvl~Pe1Y$1DTRyc zRiFLCmdk?>8+o@JNaUT6@Wy^xk3!1e?Uv5P@sf6wS?<2Fxs80kf52`Q`WWPxAs_j( z5t^K}dC6QfOQoUmHC)iqS~x4J69W~uGV*nr^lEo)d133qC- z0Swxmy4U7hpr}Uk_QDPlj|CSzliNLKWL#Ircq^`{sxr>RsZ?5Moyl7Wm~fG(G9Ojc zPSb*D@h^eC@teGxWKI!%hqL}UoyHuTgng2!+ws|7iJo1)i*ap=RQod8pQZQ*XsCJo z!MSR~tE|ykPf2|%{?1c*i>KoONw2#K@%$2I@CQzv^A#1K6(OMR(Lcui(>({0z)Ivl z9>KRTM{SH^SYq!v#?wNpE9gO~*QfI$O|B}D*Vm0{fMeu*qt>^W zn#!aAXR9L;BO3hM%RI^Ylz_N4C4}?WSS{EXhY85A?#COQ{Lf=6a0d%9EjrQrM17gT z3&6%DGc9B5V=lF5B7=K0FX$^9S@%r;2!QMPqU}m8#g5W2@}5P9aqWvh!~t?(Or|H^ zF+GoBz{F(#;yv|9hhtI;v~7lR9YGbFsU_6*e5E=fuuC|`6-qF)A_ZclmMgt+=W`zF zA?Q}1cGtPi{^C1Y<&=QYzc9>tMEo$g^!`%>;_g@E@88$h8DKz(O(n;uY`SSzaFZhs z`jSN@I(G)QH*u-*y5jO{WRx6V6P}iKT-qT0djm)GDcsY#9@YN zi2uzhAqgsn;jmiEh~77ypHIW-o_Fu2Qa&7H+du8?>&`-Oc%ykY@qMCUZP5Pry)Rof z(trZD*?d#JfyoBDT$Kufnb$6H^{s7Nua%mZr?(b6jvB$Y%BoruvoT6792Umh#Tqk3 zYOt@o_WuiSRjeoTWoZFYm)Np_)K9EbK2rw2NE3}D$9$W1eyd5HalLK3j}+KvyB?{q zXP{!%$F{h_u+MVJCmzh>Tm#tVvhjO)FKN zAXVFw@oYRS?}^V)LAc9&?`e(!rup2vzN_oGQSar(2c6IN2N*n)ec>id7}qBme2-wS zo=2^=<;M#X5G7nVytjmkEcl*0M#)l9s=@Y8ZmO@%!A5Cwwfh-jE0|jDIFGlaUvGOZ zoDXhcvWb<(XO9){4fn{TPS>U7CSXa0L@6&D60BBh15w^fH+M&}b9IRkaQ{c0sz>0| zgE`-)>!GwMef%u%f?u*Zd_;P-=x+5d6Do&ub{FgsabdppXIQ-Ds@s@r3k# z^8uy7%bHl@mbrdO+jHfa@pI2c2ODDKxV@Frc2b){K0$8zsK`hLo}S4zyA84#^m--` zv_HBY^`B!myUm^OoOAtDWRtjZjp?$ZysbdMowyV9+O0SeWvc5AJp^bQ4oV`gI3K~X zCbAj7pY2`07&1OdaR!aFup&vXbHq>p{QnPFve5FWHG-m=$_&Hre0jD=oao&ZG)@(D z2R8jRX_Q}~J@R=Q_ugO!f#(S^GuUa@M5~3tF&r0(Ke^rJtsfH2J6;Kw#;)LC%Eipu zm6k&Q@$7bp!-^+ON~1?Nw&@mrkfK~dm-$1H~1} zH#!5uJSJ+{*1o=iVjwz2C{rN-_=%3P5-b`|Cc7tT$$d`IrLsTS-w@DjUTTC;qql#g zGwHMcYIxFmA;^uU{1yzJ0i~I`7aG}=z9~dSn=izq%S^7j+QVl3}qEt z=@F@_X+RKw$RgJcKIbsH=aT+pa3aF6&0s<+&9&HBK9Fk#ZRY7W*d-~~?Oj;=<(?nq z^R??r@wXH-Z~64)xs%y!Idi(qcm}ze`pBS$;B`<(Gxz!>Gm9Fn+fZ=O{rL^+?Cv-g z_k6PXx&}${xRpL#gu>&HtdZW`S^NX~=1`3$`-8ics{NFG+EtXHbpE~Qq-bq1E@Qd}O z#ea-oiGA1v!`$ajPnT!Uos^y!>X9(9CjPv7oqsAp z!e=vl{=Zj$vIooa%as|w~pjNug?}TVg8Qmk>vb!&mmN{f>*gpTZPPbR7P{-BsdH@wv?#hHt`jDr?Z~@XsWJMJ6v9vFWk-jp@uWa_786Pv0ZVUJE$AcT1vaK)!1&HzU#^hU$cq4 z<88*Mim$X>JGcRJdg;q)+jkuev!}~Y#T|u5kG6D$qr`8UtT0qO88(f&OI&4ac-_@{rh-fKW^zp&H+uNcGWIi~L zDRSA3fJML7B}#<(b&7ZiI^K3q&4Tr5@}IXo;Hx$zQjDSjV>1Eura={vV2ZbHyQ;J# zMceDvXxKQZ8?s_;Xv=FS)1!>TA5X|p=i(xHsmv06kQfYj)~aMbhrFb4?+9X>e65pS zmnmgu7~3OiJMh1MRKf8_^MH(H?Jq;2M)POwW;}ux@Ppoz9z>Z*ke+2tg{-A1$;+(C zy}xmXc!!`%h*NT`vFckc`1RszlT~#8667gM2Wx2C12p;P)qo{bv2QQiq4Zd$0<4R4 zN4WV`U#-PC6a9nyYF->t=T{_S8@NAnyfDhiMpk%|nkOKr&@M`IE+Z$3=9azD#S@+8 z;j8@lX>x78qH(m^$i~dV{^bHhDwsfkZnV_S5q74!$?6c2s04@KFu4{pw(E2Y zjBg+H!z7$E#fK12exo3pAW-$vPP`!cnDz`{3nyk^k+_Wh3!c%=!NJ{gFkA7P3_@rA zigP{iq@_gP5mWmlSPPqSo$`p+LA%h~_xCq>IG}p}Sp24)a*gsd^`1F$=@uqqaS(32 zSOdo-f42#+-6z)8VuXYrKlBau;v}nZ@k(a~zEhOFeRq!DHdhqdy;N>z-9cV7)G@nM zZeQaak00{8aq`Kn?|bL^QrrA$JC=~73wZt-F5uGh=TvCj1#28S1eEh;iujAl-yRqn zB$*}GKE~FbtJ=w;<-%Zy=k4^}a6V(1f&sBMJ9R+Zy{??A$Ak^&gCKO*k#$a9A>OSF z#h-zca9e=)R5;fjRRNB4vK*0~Ma~Vr(nZO0X@Q?Q1@=Sy(|^(XUz8jyH>66$Wj4iN zz7c`V7fk-XY3}cs${b)wh6SE)y;D1V8UD(b6;ek zXQBmJ!9`l*JxPaRo^;y@4B@~o6cEE|&ABF<2!O>G-Qx<{v%U9}NyM_vSiqq0|1>)np$in1sdJ=&#XwL6lNpf3w|`ZMSU^*Mrdkix_5}V zd{IBrYI-|mk5t0>a=#B}a>W!=BbNej&*vSO&1ClaXGz$lO(YlDtbeQI19L)i_d@O| zI#qBe&G5_*+nWu8KP^^-q&=;#9Q=I8Ba^hSJ^TFjIk?4sy<-+up6PO*DH0$FAINfe zS1uCbDF^C6h9;*AE=NC)FKM6=81RBTGF+<*+Y&tNE35x;X&75qA7)ADzFmFK;#4&( za&sx!-yw^b&K=knEVZ!oacIETvs_Fk)79Q{nQ*ebm~lM=D8_9|TLxS%;G|)t$xWCJ zX0%DDG8B+4A~zioLBlw*J0y$j;dKnZ8=bgXRvorhbw&!4;dIZ}0?WKg@BjK~Q+6a^ z+lbestE%91lznII1F&Mgc~N#U;dJnQ1SVIt)z&Y(UgnSAjI@!I(@&_ZNkAmF@1K`KmzNv9Dl}`; zj@QWJyy`Q&{i%=WS3g}KwALDO&iuFR$IW)Shd@o=jumPtuQVFk$q&2kft(#I1E{vNjjXdM?NSAA)305r7U~VXW`WQ93b9hV##bxR+w()vShq&4b|B< z#!;}<2!u!$>LM>f9gRB@4;*Cc<(yw<8EB;L$hYmTL_QcWbCAJIPt%iK)$%SE72Cp+ZUG~gC8nZ9#b&!QD`X-kalXnzNidi5@;eWRGYQ2O zlcoWYitoS7>+iwF#wF*mym5#mb+wlocrP#{6I-i?s_;_Pl$|ImlqB=KsMVcQIb?1DTqNPL%dKM;etlx0XW3c7`2%KkNY;gWbW%kJdV_DLr16&}PuVQs=tSJq$jlIE8gD6Rg8t$yFiy?TC*`&|OI#ejzpW=_ zK(;;(Tbv^`5EV{xP(4~dLCv%xfT zNZ{m#8wD-ZM;@HdFM9eA3~M&m$A7hbaJQJE9aJ zLtFeQX!^RpJy%8P7KoFFN58#nK;MA9FtiAX<+QwNnx7H1^owF_lKrdIe4^;xVd)3l zgjwJz8o_KBLxK`iyNL=liLmkPe7Q-O0+MEp^MG8ewClnIOdW-)AVWGdp&BFkNL4Hm zgfr}5VZhH%xs#gnCN4UvFiGThuFasfDwwc^u-)zGSZ$K6MUZi2Lgd95)@N(J=@zL#47Dd3O+CQ;3}gqw{mCfxmd+HmpM4u zi*VZQss8?)N`Xy~T;>E8pA8J#yN=h3kh;7TZ8+5&f{O?D~_mq=Xr@gw-=>X zDR`DM?~ZdR=$P`g&ZB4|UvBgaG;%Dp$m0Z5t7d2GLzMbcz0O*Ur=lwI^Mqx&3;!r0 zR1cV0frHLtP~+D@axAIkg@AQZ_M=BH^B?*!6PUSz{9U&&x(G4X#6Sr&f6ux|P zWoO0}{UFFA|1f|r++_@7o%mrUc{)(1;zpL$pk#Rv^ zpakW{(X1zzvkslzH@E_4ZVRju*AUdI%D(VYzxv9ebG+;!wMat!-R{OHx38v;_!R7Y z%oi+6UQS>5rY-o;S*Q4DOL%^H5M^{XTAr?+;qtH*1?L6UJ_c?j#d%M=i$SD_D4#^x zSwMwbw(_vO++)imJj#xmv_KZgBU=+4_Jmf+1GN8|X57Cou{1 zAlb5445eiihp_Rp*0Dg}u*R&ZcMSK@|7 z7td=4svE$Dux=Z*Gf55)n|5Cq5Gi=g%vwmigH)^G{JG}PH<|5o>L)oi$(}K4)9_M0 zDkQFg)~MY;sY6F#d8{vC6j_vs2id?#l)pULCC7o*I!8|?gmaJ`s_Izmq-(!uy zbnQswSbvM7Gmb*uaND#J)lp+~87CdprV6(iKqFb;M7k!m_7A$*?>z>m6(I;oAS8X? zeE$;g=qdAIN$NcjCuFHJwL`-C?ZG7|@k2|lL18pj6}fGn{Y5mphmt-cXiMRh{l+ z6oKggbr?+9y+ADePm zo6P|IUV9fuZGYyrHj@cUH*f)ULi)v(t$~g(8`Is6a6bgEK!@#>9_{c@dR^Lhb8HyA zl}}=QV*m3{3D>8&AETCoWgU`K3yQG!R8IYUohpTj=k+-g zypA;lvpvUQIX7|nCSS_`qvoxm;tJZe!6XFN;7)+x?he7--K7cc?j*RoySuv+q;Yp` z+_jO$VOG9#&dm9*=3=hq`rWnH+Er^;z4cg$u)JkjffOi7Y9*Uwe#t8==n6!^@QlNH z8eK4|8pv{y3Cy`qT_U8#OzgfSS9uotSx6NkD$VQZjjCb<-z6njSz?^eEeK=%F~oi1 zmS{f9O!Ah38|n>D4EZJu;jQvyDSG3LOp>E3)yZ%(!J#l#oa@RZfw}m>qig1PE(HwG zT3qs7633oUv8mwUOjV`gU`g!t{oL*zRdUjnpC-?#By;KT{U1kqre8QP_BLLfGS`XAu5%|^yFkdl>A z1P=TmAtm(~J@w_&_`s-1Hx99-}8 zN7_^w*}M}nkhRn~gK}|Dw8B4NG7p0UT~JGMOXK6{C1w7>=etR5Z3C%I*?0da&5yj8 zX{N4OZM5Bt)#n4*l@to4B+Y=5+rQPe?u3eVWG!-?`z^hehYktLcKMFAf4GM~r1r`e z{q5!_c9K6Tk5{RVmi^~T(jRtK+~8s)d#Q8+{)=MN6!W@+Cw(`JC-xQDut4CvC;8r` zOK?tvl$Xw2Y)b)-B^e#*(nxH`KAmk%&c0oej{Pqpz;XWH2sqN9N7OlLzDHzb*xvS< z7y2!MM261VJ;wh6KHEOq9anVzFQ`210~!ARP?#dd{rumW(biAth5tzGPufAE|3e{s zWc-_f|4lahzi<)%zb!k6LitwA;@SHP@e?6YbFq^DKVL1fv2XV?Z#ra+NaD61r?(!Q zt3L5@sEb_-8Vt!5Iy~vr-7fBZ=RwCQX@l%ktIK*8SOSIXE!m>6dMEQL_mHRln+5#$ zM(*i*o zyjIaakRLYRSXff58+EI>b0+CNzEf2f9LkOIX*bIZgXgRph5jER$nl^C5GzRZN7V2r z*=@Fq+X{W_N};flfQ z7;o^sdkPx5hwYuTYvS!gMeVrQoCRL)>Kc%n74+hd=LrWYd=T8M#185gekb{KAgKib zYrd-D9XC=|AIz`%quKX_ykLr`vCf$EbJ1(F#y*6=Z2YyJ4n7G8z1`^#rcZx?o3z(b zw1Ivp0X!au+CSaZx3n4D6QX&Eg!c5g3;9mnftue9>I1MiEs!eQqr=>P0 z?Dvu>j8Eaqu#Y1P1Y8L{EPK5Mf9HZ5+iF4n7qpIS$`0qxNQf!)raMufHrc{|d(5Ex zdB+>SYF5Yf3@%rSykC|8a*jC=)_3IfMo_}X5w{q;_j)%eW#jLe#Antc5eco@-aBg8 z)p?}iJ+(UP_38q%T61{vVqhi;DX%l2mn`7YeAV1aD^2l}9z<J$UCXEwT^&}s)uA;pG3l# z^Kt$@e>AGM5Lvhef7MY}3j&1oxm84<{5$hbp&^(tWPPjG_7J~rd2UH+yTvN#@r#(^ zt(4W%!uymrIic-d(jb>5RNr&)-gJR_QfAjf8N6tSG+ z#O}H0t<1VQScQkO&|Xh@T|xG_Kdq$a)zSmENA431zfb7!#V5hLUqkHJLLf}_hqeR; z7lP6E%k8R#Xsu*G#yG}-E90M?4zK!1Nk^rQ^j>^tDGcWG;4@6+<)_)h7&mSbgYk~Q zCp|yIQvzA@qoBfQ1~2oM@fVR!qjoGCLZW)MwY_tXQRP}XGYrmvWy21rPGtL=J56QG zO`Bw6Hctv-T!+pCYWGU}Es3 zw+>w$lo6zP>f*(naT++PZ=TB@3KnNrqK-jm{;-Jc;mTESU|qkMvG!-!t`Fe|bBt%# zd#Zyg^>ZFiTPEBHYuy}=7kHhb>7%FAROjxVg#XYp_cgkq(CZrA7T!KIk$^F3+12UG zGz&MPQ4y#rD%5)CkI!zvB4gT({?`Mb%gO5XoPQHu=6q`?Y$*2hPVPBfce5%Ok2yQF z(-b^e=stJW(uw;p7F#70C*t|UXcnE;U%OgdmAw{o$e_b=JwQFdJ##v`+BLg8(SUOW z*xsJ)Y7!~@AjrM;aDJJmITMv+DRzf(&LJGin`K))<;+^{c}W?5ecx$@g=I0_C?!NB znZA8k?7cDIdp_i6E$4X=BAnKDkKWrGAdT`iCvLn_e!72B-r%`|$3AQZ+$0GvX#A0# z=tSGaSI)Ayonfbh*vtG*46nK)oPac2%;Y0~hMCv?%r#k|-}ouZHj&Tc8bo>*ZW zqn!aEc_gh?t~&q>#o~>(aV;f!;w8K6IAyK~xuZYCdWIeO2|C)2msW~?z&>MGtPKB&tLZqUamJ zS5siZanY+&$}935Wp+2?$9~%ZX}!|9Gb4`#K4?!> z;2ghoe9z!?60grOz0Xvg$$mK2{{7ke*zczc#3M^DY|mOZwNaoXs-q~`a~mg?xvIqdym zQ({{^{aCN>pz6{V>k$hi&+{3lm*{N`cILY{?b3C+8X!MW-hygCxm`XL8=Gp^plSK+ zLd+S!wZ>d~*D!sT?3H{ECh3_s(#pn09T;l`+hyb(ziXb?02- z+PL>vV4mJ_{1sbGkKBF_WZZ3`J&mF3OdS5g1wQM_UCq*iDLrfb{LCGH*Q~eO%D59*oZf^l z5V4-|3@}WYdgs`>J?Zb>xA-mpee<$CGDkiDeCq_9%20HgpiG(2A53A#z#8}~WUf<+ zF_C^lRKhpR(&S5&(CZzbSvWPB{%6gX^7bZB-f?WBf0#|scu$%@#GYWh?GHowo|4EM zWNXEYL42TQ?8vHu3d1kZjP(p;B=hc>8Y;LZoz9q0-)NYZTPm^|v&w%5kw94de(8C| z5%T!!_9d!RYoG0;;p~3b^)t0X5QKQF4#WWe;M}K_BYTtYI`MRkgMw+j*gDXwI%{RK zmptrXf}ud~kyz{og}X>j=zb<$@cx)GjUyRvO)_*58)dH5_F4So@4^$&U#9@^Y5wv4 zuZS>P8c6N#-}UjKx9#Z3+3Zj&ab8F4VQ*+J?W3Acygs-?ToL+!NkctC@A-8r2nrkq zwiQ_;SVzMbYgb(_wltd(kP>MIT^W@^?YE0o8`sW6*w7ZL1$_!3V***h+<~>dUsxp7 ziy=D$FLPuIB=%geNd+!3HoAT8LN?#?ClU?jxAu3;Q1)%#!$wJLD3@`E%32Lfe)lE^ zi$Ft;U@VjPA?%L3a&w_1yC)ATs=F22P;LR&Z>Y0j8kY7JkqY2z8Ulyr8 zS^3#ncw}^V*52%E`!-+7HCU<80XTB$mD{$<4Em|AMM+TI%v_Ilv19(TZ%47Qmj&c-m@&XyM^~RQ-CG z8MgG*MGaV<>}{C`{^#c8f7o$<+^EB$rVqSa&>_NGVhgK)Dzq7}-@D|@Df?Y;s#Kja z4!1_5=ZgwOS59XJUIn*wb=&Y5c5{d1N^K*m}B^{5Ud$^+s_Y_aQ5f)ipQ z8~AM+eC}%t@35q*q7{{$_~(Z8Q}k<_ZPkaffWLUDGsvDt-xfyBE8w%dk7M1wW5Y`J z6!|lnUh{m^uEa3>GH=shkx zr|#dsS&eCx$IWrnhePG)9;&o4VL3=N{`dSevgUDuu(HD6JjcWC%NxFjaVe&y6Q2f}m%qP*#cxdMlmLs6v3cWnJ_s$x`%?4`Jvkv9-WJ)Zo4a}2dFVAK6&pc0Sztsm``d`9%#9A2v}!C)McbNTr-i5v7?Df+tnl*r@_mFhz`k9tmnV&cG1{>efj z!V#INf{oeO6nCM~!c74so9z1KG3L)16xn6HOSebQ7ld?m1>Ee$6NrkAn(I1AgRhgP zy@2|!t|kjjG)QnWpCwLX)A|d$$*x&b5)&;c^R|>a9`)XFT52>5b22t^e>bSjMY^yt)}0sCJx^r&u?Ql(fJh_NapuOonbu!7anOjO4gLTj#VrY6FH!bPt2?t{srp zq~Bl7NN2^j*WA7iWN@ptepuMuYO+whPiYDb=R92hbaWb${=8U@M_$@HB0DY67YLQH zXZ=;F4i#ZY6M7u{MmRE`$DiZN@?;SqaNmDwbd5l^A}-7Z{N`JS+hl@r83`;ZZwn3H z^)+Z-@HtnU<-#8n-Q=f{lPOfXk{QvykT3j-+7ESaO0)?qnUth)Lo8iCrirptpTBQ` ztJDZsr3msufgZftoER#zITtl#sdM-rEx;P9&CP(zL)l}I{G(D%t{>_qfrqHsmwFls z?&npN9P7c=C6RQj85!w~=08Fo8-Fj0<{}Nftw$2k{csYAj}?l%NnttP$1efmZ9z*z zEp59Nmg34bLRuj@m3RziBZP7PTDHku-uvUm1Y&~NdT+q%qW+IlsN=T@TtZc1a|O9? z5waM>Y44cddZSEfVEQv9;xJz3DNuhkm{G;+oyv79kt|e8r!H11mhw686<~>hH!<8B z_`BGkLSpt$t|VFS8pHV^Z#+VS6=iPJqRC~2W-I`~R(g#yXn!@0+7O=SPs{*8e=04j zvMAZne{FMpmeC?=r6tD+r-)r{e<+d3>f<^(cI}$%vvzM7R?LdF{v$3l zb5)gTg5-W{fV=rZk;{;1X&pRxr zyK7Js8Jot%_euhnqWIXHiHpcbo0H($+>XB}F379_Pq>i=S$zc_7&Y0HEN9>)ZVkJ_ zXZM4cr5dHCz^6ukQ$=`Ol*+DWQ}YZr+wftbS#F*a<+?au7joI73({)2&_1KSpaAbq zKNrYcesT3-bm0#+@Ht1v%y zDswN2*dx8(>X9P@u4lY~5o?>ETICo-NJvrXO1qyvh7Z$Z(w=g-QQVbD)T^HKa?p#2@#clyDyg~^Z(G7 zTPDsIdpwbXv-=sRP`JM>h?1P^h3<)WkKj<>K@DH3_`!TmLuz&DV)h;%15M=S#w%0B z(JH#wYd>l0GjKY8g#XL?(drb|W{eAs_r}$LKr2%sTW7b$k0TNXcBaCWS)oMh{qY8| zC7o7qZ>dp8zgt}nHWfUYCBJV;+c{PO-nbxju`~?DDxY#W*^i7tFE~^H9Fm-DugBtD zMHloPfF@~potAq0itJI0;E9L$?2#z{=Am&tKpW_!$Jm~Q``oxtWu3ub2J-h3eV#n5yU6;%DyZ=Z5Gq3kyJ6kJvw6SH6Q2V}B$wv#(w^{+~tVm?fsa-3lpmZsi@-V9IscG0I| zXiT*niG!ynOen^wnl|b<71z+`j2MB^+Fx2RH}iMtuQ@Qyu)Sk14?$UrAQ_17z}_A^ zyS%V+G&|!<9FA8z8u3nS$J=rFT(*C=IJw;p`w-*I3?{TXi5Nte4NrwCY2>@WS;HOs z*>dD3DDx=}WF$8?C6$1aP^_R9TNxt2XXWu8Qw2GoevwY+14Zk&^KFs!DzYSW)2OFk1{z(fNMosm=H&0`tenqW{!5RbIG&Vu$nuKVH{-2$(qGsKM@K>kiP3lWzlvQV1 zdM3{8SQRtqY2O{j{8xcjdwlbaYf06}r|I{Nt=_C=3>1B@?-Na-$RF4@khu7!=X-`0 zeGBebvwqVrfwNkxAE-*ca8-c71T9xlHlEPs+s#UXN8c@azjW@jl3POnfkqX6sW-(J zt%MkKO+PMVazbT=tc;1xKO-T+X}dV_9(z=E6-$#@?xc3e_)p$FfafIN4Nec{R(F)b zb>Hc^mieo$+nd;PKCG-hnZx&PLaxvIiD^lZOA`cveZK=D{EcjpA|!Tp-UgFDr*RXz z#%1-ng3B<%dQo<1RT~Hm#}#`?zF)9T+b?!v8_;ScJ~Z5l0hvy_pk{w)>CD$T5%^ww zvZ~DEZrP33uAX}C0s^scR93spP+J8S0=C~(pUPy>JC_*%ebbpz%;gIed!Yui)KROG zeF+hC2^tzIV5s|rTioiPY@z9+t;e}B9IG@A89K^K2NU2-_l78;RMQM?|2GnQZ%mnr zj}2|;2eP~v0R(K5LI7<5a!R7ug<{`Y9iyM?nbANx6PmRNrnjPo+U4lJ+E|m<$}uwT z)MUwNNNHw-*%;aF7er?5#0Tf^x=__=N&9+LD}ad2>3sCxNC_(Av!{5o1TNISPeEzI zg_kAXi5(t%_)5uRL>YIuQxW<)qibzxz|I5(R7?KEW~IxVHq$7}ob9;j?{Et+7hl#|3 zJQL8qg~U5SE~E2<*n~G^YnElp5P{o@L^xRV;pd=CWzSsW{G!1x$5u9>Ly^snSPIQ@ zvNzviuq}S_fMH`)T+E3GyM4hLq(`qdP_%x3npvn3to8-EZl46yLGuo7t`e>{{~Ryl zj&7F~6W&99kWw8>rK5mw4Mn;=sk``y(6sW=B5l&&Y6TCug9u3_KN2mWak}$wB$WGT zs+bBdFT^9gdbkN(vnq5V4Gi(fTH_i{2~t}8v*V3FFD%jj_z(;QhRQIdLErY+dsyuv zAO)xIXAqUGKl$YlFQe3!w9Ya(V0RTn-a{YF`Pj!ohFrh%_MYQ1FNkINBZ{2JxxTs34PgQ@Yi+24i#TG zpkM^@yp)Hxu%X%NJ0Eg#I)>C#ZtqVEmO&L4H(C6aug<+0ozZ|fNxkDGV%JMDx${wt(Rw}oL`%&h%%f5p z0IQP~oyVgn>07rQmC4icAm%FC%8~J9#DX%IPafFm>aRm986G45V|-V70M5{b>LK3p zvlJDDez9n4&ew|-u|FCT8kbwIbsqG&@@YeHQL9x#`O2m1Uy08W#v6t|@_+L6`i~^5 z*4>xM8*Q=;6tk-`mXGo*LO83G`+&EML2Zz+`G*|UdS^Dh;-gVdc+56Ug|i|^;&fn=C*3}(udeRZ-*pvm0vNc zi!F@u@HQ0G)oT56*{t=qOlS>VS*IegEQ_(Nt_aWE) z$A{*~`5I9*9=z%x9a>rx;>#5pC8xds-4R8_>Q{e8NB`dqpt#mPHf(H^xwI&La%HU!BXa}xAQz_UX{D+N8fUAasRx3taM%-Q&({Q9_U@2 zED8OJE+QrMC+-ue+DX`NwRkeEpTggYslSV3s$oLYCn**qs1(D|B1k2IGVkzZM7%`2 zu2wgn-r*>Wc%^QAr*;VC%`P|EZP~6s-c(?uUfptWMUbs%XJ$W?4urlzfQRib7dr<@7g^x2ajDSZ$wc3@v|)8Pwpmc z=&b-$&{cByxHrK4Y>}t?PcaMbh}PqkYJrN^A-WMsruC7qDDvpo}^6j8))a z!QsY8fQ|@gj$R}}RSQN9l^_cc8CA!svU=RLgvlG|R$v&jR&yEc>(`ikZJkYt&Od#$L7fMi7|EA5Su@N$7E6zI!A4Spcu)tWjB zbkdFBV~15=nuwYjXuw+fk^n&8M|x6bQEo5{YYjc4R%AzH&W;<7gz_=m1E5G0mquyLI@Hss2`|TKqOnHhB>Jg0hGa5{As)BdYW|^{Sd9&O z!K1+BzAOTM@oaI54yu|Qw0L2csvABgP4Tv?{d#Y70s53HgXTu@`!4He%~!lS7wfRv zL>Ry$&s6f^{b(^gA_qrAnG05v9edT42cV%0=4>pPx)e9kCKT8T_R)5%7H80fW*%sa z!AXgTAmxp~B{yoTcMToFoYysM#Scdc|5;0$rrAVxzgw<*lG}BxxP6ZqRX!`WHR7$m zb>r#nvgWmOp*AFRAcG-GYY4sUgn;|H3SMVa(C?A3sX5W#!p>B!e9(1qnTzwh=QhVs zJI}Z;^mp$3@|-5Zqp6rxZRh^UnTw>$<`aN$wqAm@<_W{>Yqhtnj}6vLyK1Q!4<<)u zp7l3e6R2xN=7Qr2<KE^#@=5 z-oKezP0FHun%_RZ3{X&(=5P7aEXnA^!A%q`-oq9R785@zFbP;zy|3>C(_wUuaqG}- zAe_?jPRQ9Wds?m?Y+|w@(%-RUZ8ii+lMi1HfYoN!_Rh|ntWUa%izyJ|;^QeA|3w{! z4G#up&cgnLN%i;41yQYoZ>*VE--lxzt1W^v&_%DG-fXg#a$Q43B3=uNsCnXHwfT3x zMW zhBJ^8zIkaqp{T>NE2$t^p)RRLk(#K!*wyYF6F1{t#r27@!_2G4coq+MUtRikHYQ1` zc|m`Jh|!nLcTXmfFxJMqf^l!CYvl&%@JLeW=7IJENoMu$(&dg=RK>_d9j zddy{_(7{QRtR9N&&nu-+8GPoUpl|WUKOo0;3f)?Pl*xQxa8$8`PUAfoVt6N}9 z17;oeQ4I;(096$Ii&dL;0O{-ZmstS)jJ`O8IA{!bzzf*ItRLayk&B zS)5B0vUmR&q{JP76h8}7b{-U&2qSW7T>BNkA3y7*X0GbfdAA1~TUE2)BzBLMDQ7j0 zBIhTIzpeB)70sUZ$oFCjc>n`mZ|JgC{R8$T7#uD5BJ4Mz!e)6?s~bYr;|tJJRCmRv zLU6&=r{WwG;TM_!<_g+B%LU3_9=rL<;R5b4VGY=l(qm)~=-!Z!`Ib;RSI~0?UbUz+ zhjahr^HnM3kO{mTOY}r>MCk%WL$Pz{EJ;V$NQ=v4f(x=9*iFqd6W|0vddLA zD@dVwp0gm@2$~uSX7@YD=ck9F=0}V^tc%GQse64TtRTUX^K|B%Em0=tZ6zez+)A-R z(yeRTb+{-6b;owV$?p_e2JhL9ijXtO4^#J_edA`bvhopRw4%i!(Nsjwkbc&wbnI*^ z=302?0K>hOImEFS9BwSsN{pUZTlsoZ6>Wq3e3kmZtjU9l)1|8=#V<2R(%d)95i zr9$FQq9khepLo~apgi;>QH?uS^yuy5xdctGBXpAoOrlI}6`zF#sk?hz zqzVqVXs;P25CO)PTa&ZWO?GBDng;YlHs^1i4q@OM|!icEhi(RgTOp*2=z3t_e!!e%rdf?Q4L28tnZ!o7WBq zk3-w_Q=VsRbbo>WV#3Bua}^bt9ltUT`}Av#tExjU!qX((uPTi_Ir)V#(Pl>?y5e92 z?lqHRzSk679*}AYGW+3j#=4Xh@k8EPvH3K4rlb_)m-!$cg0|%U0Hw<~lwfP{E%iqF z{PkUzQ~VH7vLc)?hcEo&%@6m(h0jN~rNnjbRBJeA-$RMAf+cwyk@T2uIbS_Ars=-^ z2V{fu(Dwya6FG0_%6v*Qa>b1?LC#v!%0qpK9)dJnTbyc(9nO;R)@w_((VsrH91P-nuGIk}Q~L2Fii&G`yIAhLc0cp^ zt)#}e%MHQ%X?Ng!4C+4LGjv2XC^S9~_qO38+9~$_ ze4sqcN_!(ZBez>fDW~d&aqm;N{$X!54xzV>`_D=0@0bK&8OX2CwEJS{IR$dRo;^%#<$72%o#`|a zDmi_*zGYf}{q;{=#qSZJc;SUU^v!)v3vgu=;~E%0@zS53+znHI+TjyRmxGj(!$ItZ zJ=iOK1|(pkoE@Nv@1^V+EzLHSMz#-g z@?*JDSj)kw^NO3mXSo?hwZQv;1q|1aKF->_@3Q2(ddHW~8pYH=j*<@L%n}%H{asa)N3@dqKw=jJBJv`gr8cMM7jXVc@s!NMsv5 zxv!!`%l6Mu1zfN6bbvl)b61BkN+qLZP$@p_FrJu{|FKvQR9m{Mc}|_rkz}5 z)$fRT@y1&JT_Z-65Lhx_0$@As8XpxYvHp`b5`Tfe`R@#ZL4X7x;eq}EF0oXOG{^(j z?(0*PMy%He-hW)mXd-aR=qI&P9{W@_BT@f5eIe)5|Czo3|9>hT|G$4uC}s-7D_=v#3^^2e*8+OlphDrR5Mq_GG5un)mpRPdZ&MAAi?Iv+EL> ze#=NY7M$$^<<1T?8c?k&IprP!q@q4olHHqzzicBv1ffG~3>(}ot<}8mBC3Nh>AO2` zXU}sV0dM$z=}-6^W7k+UEvMEKZ+o&GJW`Txn#p;&pe>gFEc}P~1AAeudhBwzJeSeg z>b2j2Bpm6bk!~|%{c_BF#qgPpF3*v%`H^9%+Ue8+$ySNmpTfrYW1(TTJ2mjhap82M zeQiqE`JBGtB*M`G2hrk@t7}o8v%m?cgp@;{sE(5* zF2^JLFQT`0vylLfN@V(5d9}g^&%6h)c{|k7_peMyZ*NE)S)_0zpfDBh`m~X~%qliG zLt4b+)9^Br>3a^GjA6i2F}jD15T&s6MyzL5ggz{#kItW80Dp={5Ua-N-Pjs7V$prk z16S^BX$ldqDCP3QY4%Bn=~<3A;%!JV&HD$2(rhWP;K4@I zhIi;JRJ*+Z32y}6Y`9^YHOoTyVyH*!;F5lzy|_?osptVGcW#&^t=c-Z{(a6>C8|VA zvn)vRKyG(V_FBmn?$;a6RlRKKCrWl)?&xJ_95%OAVAm%x1$1Q~zqxf~&tPe;3n^fL ztS5^=q$Hh4;S#h0q}uX!P>>(C-(!8fQ zd@Ftzo;J~m_%>5$g^|G;GuAx|n8jdk121kSxv# z=FFvpco*GEyBI?UnT6cwvqz^(RqbFKlbqQ7?RiPu9R4PaR-Ju$rW?oOw1fIo(U-wn z>nC@0guiW-EILu_OP|}A1hs6xP#z8yZHPB`mVLTwl`NaICFrMJ7vQ_Y1C1F+!|@W8 z7rTeo^Rev>0pi(xoUUO_Z8=u)T-rY0KCj0_eLm!kl32JN|sUcb?5 z^m#L{o)MNn*`C%v-7~y32ifT|#zQ%;=Nm2_w-6*U_SQO3?Yr-gdKfv&ES0mg1%hdG zk?+!YBU`A3nNOFQqb$njp5;NOAh=pu6s<(L?OBeA?AGYNWP5xF$-F*;clq-5ES6*E z_rBjBez1XFBlOFSPFDZ4!a@pyTLsqkjbdzdhWcG`q%>%XF3s!bjj87mr0jDa98!Al z`v#%4?+BMd4;jBz5%T09D-7H;i~p?8cdK>Boh$rir=cyD1sYR)slt1e;oXYT5zosn zd!dyfv<@}Z zGLz9%hip+rTt0SuEYxd-5MAzRX3TG`xvS57s$-~SHT|C}P#hR_tsc5`z z3#5Zqo4;LWvX5!jW%u(sAocl``vcvHYbZ4Vom2?Y)pDeH*Trm6u#?G~$RWrA$~~rU zd()qiB%|6)cx=9FwwTmzseMonG#s%iyXx5A9KERW!6656yI_g%89*ou+ z19k#@J9L_?_oQY*n0U}M7v?Ji^vCn%h%cwWPpY_%oZnPq&2g_m9)fI7X^z$bk=nc# z!kevv<~DoZ{hs~k{UvF*xlSgp>oMvcccHNLN66@Ygq@`*&R4`k9mNd2kqZFJ$@eHB zK$90#xiqYw9nCn?$bPBPB=~q7j909dN2yb$=c^t~kQ%MwVWYwKnuVw$ zim%S5_k!5aLvg>BgZ|{A9A;0M zUIi0b=KGcO*I8W7C8HDwfP)u!!3(~$%B6@3(_d%Q{8DBIOrE?i*oVY_e8aK*TFQ3@ z?746ldT`v|&I0@R(j+3ddNW&C=n`TqVQG^WclUN$1N}ggXp+oXnLkt9E*>rNaktG@KhxU$%hs)iOD5zSGUDNrnzIZUr3{@@Taq;L!5ZU zL%TD%?Alp2tcEC?J$f$d@1PgzBAyIcbydFLtuR6RTRzdSifkW`g zh`M_!+BDrC%Iu2#aqss)^PjuZ<^Cj|2pkw!CCu!ofrAh+ zhLn^u49dF28CZaylZ=4BdZkuGw{Pv%y7PxLA_D$=yBO>6c0_=rsrCnEXP-A(PShGcSV&Cpf zjf)LRV7R=xn^i-G3)h_=u^Z2VOcvAZF2)>vp7=Slk;y4}lh`u-j83*%T+WxiT0&vPpEA1{El3xnKq7jU)YS-JWw4+ax*>M9Pja-A-8VuR?`}0}63!@5{11TRI>z}*SGv46Bbx5a*vjhy zJ4&sdNTFlkqp>j+iv3F@vM!1PN4d+{U!-Nx5t7b1^I!SPCo`XQs8ID7E-dcA=sKHT zoSc3AYO*QS*llMz5di@a8CN`OXkeVJDERG2CK#{Y{|QY$y9- zkPF_k#?cmZg(0XF#&V}>fO>^Z&wX&_A$?dw73k)B8(Im-g=d~{OO5^w~Um3M*lAlg9o8 zcr8xn@o>n3&sH^AV`PrZ(wPEwetUSb=;N=sTpuclUgt*7N?~$H6|s;K>A7PY9Z{P- zxm@-e1};<%C5=s?n8L#BTc^_Ek>(HLTZ6dU5Y#v2%6A3Qll5i+C zWh?iSqpu<$By!EicMMHI>K!F3UHWj+aS6BYy?fcCSYLL-?<3hr-&Tj6ePLsziI;QRF}X-=DXA zByzPRv(0&D-+~p>mT#H2{8&@Tf%IFG@T+|vAtWU-14}L^@3e;QS3@PWLguUOO$ogY zBKFii=~WWK064%vs$u<8O{<}4>FX_vzI=~@ng$O)KW$!CNroEZ-qEH?S+17zLcXdGdMaAC&62(ETLKG zgMZZbvVWB=6Ucya#-u(+x$7?*F>g5jbabtuMi^=n@Oc2&e%MeP0`Eu!duNxa&zM{T22aPmaPTVKO1P4d=-PH@dIQm%Lr3Zz_|dRIO!B zoW)a_xoj=76lVT!pI-v!XEGPIFFX^=6NID-b2dbVwUdtnz1ue)JaZtA*=JZ8^HR2GljQftR*xoEWI|L0sB$zL12GnM(WLo1EaIs|CdeARp+6RY$7^zo=R>Jd+PK=Jn zTC^NFSJJ+jWKQ;QRNQL7%})H*qfzP=^;!jBlafdzyaRp>EJnszRBP$49@d7U4tFf4 z0g-ltnSoxG@JO8gKG0`xc@)w<&ah)EX3Sm!~rBj|>4O=^Ehg12E@ zeyj+5RfIfB!e7&#-a1X!{jJha+>XE5@VNXf z#l!<4Tko{{)BUxagqjxLHatXu{gi)7RIYMpwFGgB```>%S_|m#=mR zM0aPE8~uuD25@v`O-jeeQX9ERGdi+_uRL-V3l}@Bq_i?pdL&EA3k>D*VOU-tT0C<; z6g312C=k0^tl#4CBl^fpZV!$D37Jp+K9-Z*tTiHEySewZN5FCrEFCUEbgT%ymoJA` zAPg`q;GzP3PrcLbzjYuExXrHQTcCWbw9r%sgCqa_GaBy;x3RfqxK4KHOs^#^9~Tjo zzIxe06%~`Kg~XfAqC@4b!lW4!^W87Q1jSsk8KdtW>%0nU0@Iyu8QXUDve2zVciyc8 zSWGr1l6jw_mREc4>9H?hS&MubD%2{1tS5(>?#dlBKTS*d$Pud5#I#n!V6tW9UP`v= zP`truN=JuNtd|orSgip7=Ap^qf2`NJoIsbJ63Ay_`mGTwnlAYgi3X|W+dZ5Qu)FZBQ^7nx6Odhh zUM#aQ4;ODA^YGRX!yu6~(dOJ_8~R36+t;a$sAoH_oS)?h|D_YSKnL>$a!Ik#O#tP+ z)VDf?J$yRNyj={lv6ik7ZM+piURE7??Kd5wR&A;%Zmx*mawT(18{EnR(4z-u&@PnO z1qTEqOTs?8IgLMrntcX{S=Oot@X~(c2v6e{fGl=9Fn@M|u=_MqMI$`J6ppy4aCf}0 zw?rEjtg9jnqh^ucnt2WmqgQ&(l!0t5?|;3N>--60I_ zI`|-i4mRjn$s^}|*ZRJTv(`EPn^Si^J-c^xS9Nvm`qk=MIF%%L%F%#kia<(;Pp{q> zK~=bm6#$n*O2t20-gjvlA*RoBTE#-1tv!81G>j#Jx2l;c1WExGER+j zjyD47x@wz0K#jgzy(dJDCDi;-HqjFG53_KUH&W`p z2j0NkbR!nC3jR+NggtOXUby4saN9?j6_bt!6`7|-@cJQAYBeZ&=hudIK!=B@2Fph! z=uhGR^SL}h)7uRF-imixJwi%Ky~MX$?h)37IDyvWvAQ}R>J(67 zh=xSqqOlkWh2SuFTLvJenYN^g*IrbMp|tJHPgmmJi>rY_m@dNj!~D7Bn@P_~POA2H z`d`sXpa$Xqr#p;L`5<-9AC5osmb*_3A4+G>2dH1$dWd)+IYEmrw|6)Pr%M``tuF_8 z=j|!BtSRWqzTUN3&K3W2jKn5bHZOSEk+B6w=Yg) z^3*#Ahvj8-jJRwX*sKV@8oV!>{wPRnGK`!f5gw)dGV|^MS^5{=UhIeEsrs7^(T~K+ zFL56rbn!gnk-=J{XT53hh%h;E0am-eR>QJ%79QRg=oo~(yDzj9lMe=BOy zK{+U;o590iGk$ZV@sLrU&zWh5Vsz_(q>Pc6uRpAX@P11Dc&o2<%U8ggH7{qp;~}dM zvD|3wBQ|GXf3o1zBCiusZ46UqP3xcp{qI~9#)M6+R+G%e(~Oo4w4k#hF~w0DQDJ;q z0~JS>ul_3Y_@%?5!dFM{^FgiC_Yc>uY)6e<(ovdc)sb$ihhTt6ZZ7|^jPtQ^z&P_W zy$XuUPXL;}-s1HMPaaeeV>8f@=_{5yR-162)6eH(g1!ixk*Xht5(#5~)nka%e|-0w z#==e!#AkP&l;WojrhkEJRm6ezUObD!$Tf+oX7jhaMB3;C>gwNCVf?6T;VXSOyd%kM zTEmu6tAj-cT;!0`#GwnAI4N**q=kJ}25L2?|`Q@?BA!r=( zVlvHMjfjCU52#`f+M*XR{31DjL4wI#KbCWJe;7edGvDM-F5Y~+WW37W}{`xS2i+_ES7vWA0nQSjmngi$6@CxCz+lN@y zBh*oB0LB3uGo_ZYgQV}+mZyAr=!`1xXUY%h2D08g4jfMnG$9(@0+7Dx{!GerNGLJ{ zeD@@eN!LY+OUe>%58#-ueLtf-y>PL{hqs(tcUwxU*s0dd$@Fq2hm5!$moN7w&QO|t z8oU78F~Z?!=1;xK( zXXd9&q4lDfVJ9U&%kk!kYS8DC=462*qjy^v>y2K_-yWqt?6Y&#R>sJLcFgQgJy(uq zeoY4D6zcAFrIuckc*9D#H$6)BSn$6>3kJ z8^6bHYT3H`)^#QjU_DmyV!+VC5F#w*lpifp9S?xfz8eG;+Z*tF&m=DWx-i>6JBVH( zM-Bp{OtS9UBDBvT4Gy4JO^I086un!xMdQE1OyHQX&}l|)z~@nn5?Ac{WvACV{NbgGqIXdROH%lw!qn&fRsI)NgjMo{RAv~>y@q2rZ5GdxS`DRo7ky{ngXzVJ9V%FtOSFUF>sc)d2{#YV=jiM;=ad_a3$vCjgENdM?gRK z-L;lWRrjrxP>4L!(sg99rP5B6a(QOSXXhsT5d}YhWOsC8TDy0$_IWxjj?0JPc$SJU zZcdlf$yW)M%iNeOjZlT71oG;@E*R(kjj{J_Z6TaY>Ylp9>)O;jEaBJ0y>5vSw|#u{ z_6@Gl0;XWuTjHMiys!kPJe-7(I)!#0t* z+7=SxuKQI}+a&_u#Z-IJ9tL@JxNADy?J0;LsslIzY14QID5{vz9LZlbKg^l_$2y(+ zz6JEf5dqGxil+BK6F!NBu!|%s^`l$ro@8UUD=(RnNpp>QVf=F46s$ZYwH9Ot8*e@( zwA--vD(wdAu3Jdt_Fg>AB!+L=$sI?AHxrNs44h2NCNe^$#@kafHlym>nPoquYy%Tk zRyNySOn41$y)x+ISPot%8BW`7YQb4AmqSS{r%H)&lARR5QC#Mp&J z-9yAnlXM3sVlrAzLOJr+%###>B$!6CNu-x_PwVK*+C0rvB(uvVsGXQ5Ej0K0L`VG*9RiXREv zqk9gQ;^Xb-GeAgu%|ZOfHPrz#j;9k3>dD0EjJW&D(wyU+jw(#+_ZJo%YD5|Em zdqUtq0(ta8vyi2^RRZ;aOHVHvQ^S#4rPXB>FVlY*1TD;cD>4#3hNn#7063E^xr0`> z1)>|_{m?LXe)ab-r}iKp@598}Hrs!0>H6&eOv=2zc<{FS(V+z6u9SfOQv>9Wj|2j` z8RYzHaKfB(?7N{%;)*r|0;_6|3>$w;#Z+bz>*8-;OO@BrPn{{DD{}4jMw3I57cZ% zV%fzyR-&fN*L+)PW4o$(cX||<%^mgHrk2N)rcAy}k&@dwo zjT2j>cqhqR(Yn0*aBOqEioUfXX}505)|~px&(Ji4x@;47yL$9Av zhxrtKaNoGl_6R&!cXH3D^?T=dINjt&wA@%cU9Cu)4Me40c@-1rbRab7c4b5-&+?qb z4|Z@t#2Z2}`rX1tWqT=!AC%dd^R>Kf2LFGC+xcCXS7+?rgC%lUJxxn&YVAMkrZjnN zH;PU*DH_%@f&(r^Bz8&cAL`P)XZgGW;RL+*&$JWgio|>GE;or?{Q2y@W$Sf*YChyh z`u?0r&(CZ9D7f8r!zocfIKwh4t=e@JDfz^wLAP{fgSHscDJEV~%ewLev6(URY*!bm zR5ZlLcZI&sTS%F6(5hvUE@P2FDVjF6onGh_VEm>7LIgJ7|ADPy^hs{qJka(Sr~6mZ z*5W!}+zw?X&t1KXa55^Bz|8R_hKwIy&nQc5)U+YoE{{LdR1uP;@#7WWigwX|<&A{z z9zv#>y(9SX0N=fcy4DE|-_<75+smUN3_hA2XXk_wF*S7Xr~2~*j`5NKfoPp!D;cV` znxTzB8F7Orp*I1EtS)0j>YTcO&u*R7*tAK966On2tSiLnlbZ{5oHBQfCC*m7Q_>05 zL0V$BqC5@>{#hMu4@#`|x(zgf$0UOd;sn#o>VS zaU_ubp?ASUsyxbZXVjsU58jk|0_p#p4)Xj&qkE~9*9Cn~4R8{ZVJ!!MPap>wG>|{> zT$oasE(fGvrb@CE=B>7AppS8?SvfCHw@JF=OCfh%Fw1j1#?1u-eD7k(>Arvcd@@uR z&WR&p&ZNv9v9{=~q{zQ8S@rb7WPdL<#g2q}P@u-_(T}5e1(fnXXHwwd8NM%X+vBmN z;%qF9sG!E^H12elZifxg3=Ta%D4gIP>Ptrmzu=VNo@Y{&_~VkjhS?(B;sHDabZogS z-m1$2uY0Gqq_;v2Nlmp@g}0jE+}s}!_K_)=Mj;}6|HI>K?h)&BUg|ZoOphT|p52jXVQhq)qHw+R$B>_+4Qu{**{^8Xe2XSY1yQ+UW@e z!fcn6k~dm#eR<5-os;@$BElqDqdM&D^ZCS(z~zm&ldYj{;Vt;BCIkM#mD$8ZgAhYG ztJH%5!I#R`-d7}z6mb;L{^l}D-c?ClSPaFCy9&eZoPyHirE&QRDjPXB`eB|#kG+Xx zF&T>xPQj&5lKOxb72pU>-WhdZ?*^|uvLifFU|lD3p`Ps5_Qo+;o@6+-4KDYkPXxkA zJcKtTk@)(~YRp7u@5GQi1rajAif_BH5MJnmh@N}?=Fl5Gbb#2@)KL0r740Iejhuij zr?3i8o>l+lM^c&IO~(KuC0fyZa}pbjbJfQE?AmnY^7TF=)Bl!TaX@(_<#sUC6dhQ- z@Q8?Ojr7U+s2>mtwwj8Rmseep;=9!^Elj+4z3RR=jp+nJN8NM^@R zqA~8r=&z*>a+vF8J)|N1N!-==UhV<2#m{&j>l|@few=!=g+M$46Edw)7MIbMqNm+m zd}hXg?q+cfIs=`K7hAvti3Ppb7A++#RQHT8%Rd+Nw{8*ZJ)prP?h;a>)83y5ecVt5 znL*+$mH6a$m1~CF%0E3?Z7NmLRBD6-{HaBYw~?f$`ZZ(8%-)#dFfDKLpqIXmq&3WX zT(D=RC7}C>4a79cAxLt!>(g)cy+eNE1v^#RIxg^5&3GSmNOtxm?t6kW`;Q- zQoF#8)*sJfrc@VqtDo0stl76X?BfK`|fHuiqsq9yQP!hkB7j036_Fm8f9@eK=lz(37YL0G zS&Qh4cdv@J;hHKQd+&x-m1(${bKgFtKt=H0>{h&bH+dNdv@Kg}V5dkj)H@wf?AhFm zK3(qasfRS-6TD|RSDCja@@C&1%t?=qif7onIRP0QB`P#RPWctLdKTX}2H0!JA;vf% zlK3OT=8H$(+b&=cqBD6(%(z8wK$|m25q|nRM%pvzUOR0K?YM4gC8J0-WZ}Buy8*aa zA6KT5#TG-VkRd!sC=K^S`1Y3H5os{ZW%#;E>qQRdEcgaX&>wvOq{oxunXx$A zSM4m6)Y_QJ6|46CSO{zUmx7|CN5+NWxTS&Pm$vx%Y3s0;P7D(J1C4_NR3SkDp{^-L z?zL8B;aK^f(YmvR)0O7^Bo-W%zbP6c+E^ROx$wDe-guhWO+9O^_%>7FG&U=FKhzdT zQ~hn@!zu}>HV<#@JE+P;zS-8Xny)fv1gAlFt03lNhPjg$lBfex4MJRBSXfe$WPBVO zo?*)K5KOsuTCY%*Tf|uvM`?Jq5k z8)(bvj^P}2hm1{X^sh!k-#i7tpUH$tA`o+KY}g|QyMk|B%-w&w4`8nS+=?EaR3VYH ziU*Ik3MEa4W0KDHUFbdUs2_228kC#TK8AmZDCMGg8ea>HRM4moBzqcj76`vUrVe`{ zjoSYDZBl+n3{?~omel*Iug|^Y`Z**HSatC5@HAwR@Fx^rd@g3nnNX%8*Ho-*6+NL; zq@tq7qvU#&nD6&Yf`T&r$3WSmD}b>~CB_P9Nh?PR z4!mPn|MS5Mv$`2H`o3h)REE-gqj>N_Sj=nwQ%Pe|eZ8`-?{${I#e!*<-c9coAvsxD zS+ygYw->_0rf?xv9g?Poz&JH?PgiazUO#X+g4ln|m&BU32|f5x2h;;zeB1@TW#-~u zvEMjyVQk#At95u$&)J!JN-%nMQDZi~qIz&ryU-X!>2=N2^Kwl+)K!;@qhPi+J(xG) zK`W23bum!Zm?XlSyVM^Na9{yB?csJbqLl4@$$*RPn$eeOxlj+L+PlrHv&-r0&@iUeN@=%ENRzjM}$0fVG8eL+^wlR7cbCPT1(mp+hrap6%oZ4 z99F6}-eleN&V*tjPmWW4b9%2W zCCy$$nW?PjO0~pSg?#ZSfn{EQn0ZsA7>zeJK2M^bJT`yNeP zhHLCzPro`>?yBETBylxix_i=|N67dJl!P_fknLcLy`!uXSxmNe!+*B#Tb@X+c-J7+ z7p7qDoY#G<`D;WZ9X64g)~LEUi6QC>*@@IA&WcMEx?haM}v-jmHx z=7fQ!5=7PKTXq_Z(d?6%5PD?9tp%+gn~~?f66mox0I<=oP%Eq=!&Z0a>WjT+vB#;l@--lCE%2e zX#=urw^-&<;oBu2EaqM@mJYrk5xhq3I&BZJmWjY3gN9~$M#TIsg3+ZaHrh2RPDza0X0qp2#>1#65D2diKl&P3VGa#l@t;|a1BTdFi93ho!-705y8q&V4x>H)s(rkO=nc`OQvd()+W%!w$SfwHd}RU)O;=!Z>G*q^suvZy*Qd~ zaMuF`wCvuSKmDj_@62`~pfTX<1zcbkf&h zqdOF0rMprL47=waLVXzYm-7|Toa9T!DqbzNC#d=|^ot5q&9>8%SK7*j_AW0Z2i)r8 z{pitbjc&Jp8_N8*7uul7>v>>MQ$t&xb7I=)^1RbqO0hB51PR_d z!Yg*LRGCKPM7PV7CzMuJfK&Mffk-MHcWs6;W)7Pd$WZ-5!e(&%2lGJI$A%o&QR9(d z)2FXPC_-1Cn%0fI0wg2rpf4~U?#VNe7&3%E{PasC2dmLLKMniGm=O`#fonF76c$f6 zXrXIbFCrKvHaxkxTpgBDo(UrIT2g1N<#x4r58||Q7ig-ZAZC>aMj#}VSI}|#r;kn_ zThGo2Z`;a7zOA>gcCmy7fAN6o#jYMUx^9OjUImsf?-d57TW>44FdU=(d`Pa4tYK=P z5R~+I*`6EyMQ_FGDnNOT+Bb-ypOB>HuOF`ZuyLobySgD}>+8ql9lWw@Zw7g&F zsC$jcz}1AIDsr`R;1$eh3s!a4nr6bVQ%x^@)An$G>LIL3ERUY=XVgs}FO72QG5Eup zE?#sI8ozk@+%e_`4irjs3L#Sp$^_8u=U2c_mcgX~p5Hi6#Ky!R;;9 z`c}|NkkCS7fN(VSLo?u=vh+hh+2SdW{OUv+t=%i$oE@0%Ce%~Z`^xc6Ri31$0pJG^jYRNsKL*ecAo{8Y-v=A#iY||FWX} z$>7AAplsjkIjI265Kk=ND8{?vba8<=pW%tM>CQB@l=^8NF5l`$Zj!gYH}GMpyrYD;K>Z>c5)nbd;{*d zTxj&tS{%`O-B{zSfYtufKEZOCz+!Bq$qhH4h3g_sZ3gw6o~jMEacvGYk-hWO|2v8` z3oAi+H`I*~6T@EN!65iWq`=oz{CRh~1=GVxkp4L5p-Kw|WxrKY@D1|Jx6v-ceZ%WL zVh?Ml5BN*fxJIT$C5!Ufn|vXhkg`n5vzb)>aaX36sHmuWQll3#-mJTOu|nOHtVJIN z=|9`hqN80a+xa~?`7q{nlLcB+FOKBxCN4PdBBkd)+^|FF zE#Orh^px<@R+ltlEv^vZREfEAp&7^cq%7q}V4Y&1S1y3Y=JR5HfJ+7HmNmqTz(nN$ zHpwQpPo0ks%~q96TJ?GnbQwAM_|0Kx@sajr5ImvIV-$%=vm(RJFaZ+z>`pll?|!&9E-wA-QALDtV&xH*+w>sL7K}Jt3 z;Iek;wkt$BQ-3bc>2pDC*jNUN9El*kYk3sb0XCvAn%Uh_rK2APTY1CD0zdY*GS|0a z(6l|!`}pZ`!JfJniI1m0z7HGB^Qoyh?I5Z{oMNF?;60 zs9BN0nFI>KwWMn%{M8?vX~c`}6S_B}e;8tW;iB2V=?)`U(=1J__PY2GdL#O+k<`HC z+uLPAjB>=x880k;EI;Q=SKVGPK1GY~vL`bh-_$|m^=4F*JficRrRQtEQCT41(2Qke$HL-Uu)h)MvkI3(*q;z}Xh;-3H92D3ecr~!#>U1L9_UZn<7Fd~U(CRr z_fntSY4Or?(`-8y4l=B}e9`Nk03=-;*0;Yb-XBe$D(6ewELS0;pq@G9;{wbjr>%Ff z87M)oe%MadV51-Vkb_t6nCfr0@F(%^&Z`}byoq?Pslw?QB#y~##dhfr@JK&A-1Bj} zmEnC@>INq+Au?_y*$r0TiRaH}tI}uS9T_Rk?Wm%Z`ieW9I7zE@aITquBYIDFYjk>e zN5D>l;V#4-jZ=W}xP~m0JTx*v7f3_%gz8*@%*6+#>U3FCSL%>*T_$ZVNEe-)9)Yl? zbY_Mc^B!+~LnV)hii(OFlG~$sd!c`JaEYAbH$TV~JDd<-zy`5d$8kp_XAqAZcjF`h z42+tMDu8wE5+oqq;I|9A>hFArjuAYo@NGV|TJpOoLy~oDpJx^Z+~HXdyz4QnXZWh^ z!j>TRI%VFStGTNDQ+|EleG^(DYwHI23Q=Y6@>^D95E=%@Bi_@Y3tg%8i;lSuQv&$C zir^d{``wLx=nYFo@%%-6)B1QmHqVVSp7F{OCp^qR7kXODRmi<_NyH;L^Jm#OSB)XaDHnk4Gl0f)-gc>asEnu z+SBZ=h9b~`@oc%r_K}~Nt@op)A4|*T{0aGzsX^?N=XlNS-RM(p3zl%g?%p4rz)nI{ z1YG2BrzE3uRt3bOA~O{CK2{!(o4-K&dQ`eb^m2^uBL$6A(bKec-OqBBs*BY#WlH{EaPadxKmea&vJX(QjMRRc$q%z%OD``|>pO|CJJ9Ni5x@4fXbS z6frkT+!d`kE~j4}K`UvB%+HXI>z4t&f!A(;NDVPhZ@rd~AQlja8o>IxDtzkb%EsrZ zwmINwbsqF^7 z@^m91;+yM`{Y1-A(d0d+E4KJ>au)#~oc$xpq0Sk(q&=*`a^Py)?s-rSoPaaFGil6oW&C=;mhaqnx6@tKR~n=Q69w-~{$vd5bE75-(&gE| z%qsJi2kWWvcs~rBUsiADnPa;8|I*Q6150b3Wmf1q>Gd@2*DD&~y7I}~de1~}fYS*( zA0pC!qP(eG#reH~g(W#czO@pB^w@8yp=5avyn8*f=OZp-0Jr4-xQ?;HQ-vQ}QeI+q zqP9u4YD-2CL)u!MH@b^P8k8j7wDZM}?AD3l?kHQ1U9W5&BC^B~Fk%dLp&xyL0URV8 zxjS`FOnAWt=hVq!C4g>2J|yMrQ;EW`?#3BRu;Jv9Mi=pJrCN`oiI+q|HrcsK7k@H_Q)m@bj59p%C9=ek2$q%TG{FoVU`tFw zIF?-m^KlG1K4#Hx-(&EDocusv-(=q#sF0H(7mk<7kk*qA^Wcd6z2S!BKJ6iv*gZ!k zP}cWGSCB&J|M>D5LpaioU>yM~fr_ih_)T%w1Q)&%HXf|wOI{)C;RB6 zqI5o`s5;#B0)w8y>~6^u3f%NO?)1iT7yzopk-L`3m3(UijOT|R@qIbP66F{uH_Pv>JzZIe<^@1N-b^%Ng+IH) ziRQF1)m6y+rLuKLKP1bY%ZTCoy)j&~;H2B!f=U`SS_v$F9xj;8ldv$FVIR9b&a8+?uH`4_D>^ zhvIgkKYw(`6DMhoqP#OPp~UWvH23Q6Mv`wfjRs+%Wyr#+b6 zV&|I`e1OJ@ixl{Z# zE)TRw&4}cYp8L+&&hw6rqJ8^WUK^YVNWt^!$cOh@{1%O3Tmq!9{L7T}cq-d)Fzs|F zGKHAXRQlcEhM3qbg7ZocC9jt#lhWddb)!diQ8XxEUD7A;Tqsv7k{8}stJ*cQKDEr~ zTF)IxIPfU{r9V+5o@Db&c4Tz$HNB$0Uk4hkD+1hreJ8Q2y#hSR3n9}%QI=H8 z8L&3y!HC_@TQmZ1$Lv3n&j_;rA9lv-KLX?+phc2FqUcdy}>@mlL`*KAR61_eLqUzaSTlXyRJ!U^qQYinT5%2G-JZx`s zHGOd?kpV(9#Mm-8;gMgJOe!(XU9YaRtZr>wZzwq~0Nk^gw44ZHG>1(F_O-8&*qW~r zlTp(aVkU}9;+&*Xp8dL5-ENsLOkj_`c=+MFNvysWSkH~BpuRZ>s>uP^0UZOg1=OC| zyYA8*3ewRzl!dBFg37WW{l2AoI>+cec82 zE#w`;{@Gz>t1GJf1M-Y`H!n$Ve*}8UR2v(Hjn$_%Z^V(HWd-w8cNOIAij7#}k;T-{ z@=DOWfM%}S!sYLJHsc|Q`g%b|0zoipFcc+!erGhXZGCuCJ5;>ZbgLGBd7+gvsW-ul zqqTu|%!(4WF=b3wROD*5D%d7&!+CjYs)|b5)Bshj{AwKV&@J7-VDpKfGu%b;HaO5m z(?!p2qC}FNm*8yd!gPTkU3TSI^}d6>-oAY&etJ+}<7L7gJm3cG)K^lRRJCuo`;??E z8|t^CP8`d`!d6(N7cKGoAWwqrXbr9jfm)sNsvbte+RR|CmOW9IRz_(=eBJucTxzHS^GHF859e@m9p zc^iT%7j>G3(^vMCb{l}G#F6qk6>6mL-D*t1F-G1*u+2DC%dzcR-XjYp=Aoix=lRtP ziTs1BZVz%t7gV@5lh;N7AOTFJPU5(^h38JDCvTaNMW>38FU7`GEjuy>Xth{A(G+vC zZO0HwemJNtAfBMcOXT%I4C57k%y3F2AjtJFe3l_hM-O@b9p-V>|3QPcP(T8Ix&Fio zHm5W=MxrE}AqCBSAY1)Bybw^?nEX5Z908NTc< zcKwZ29lRJFjM*;L**YcsIW4ijk%0PKE%Gx)k!( zt&?MZtDgx2V}IJ9fSw{i*q`ORdX{vDD=jH=Bbh8B!l9^SWh;u z|8VlE#^uf^n?TSlo;O=e{z$4WuWJt;vQyqbpKf}xQTT^+yGDRKVkSE3fZI#Rjir@{ zGLsuwW8Z#qBJw()8wEg!Tu7}IGc_?$gTQbHupRfFTJ4z%nb+NLqd9@rd%du4-&+41 zl8{lk7W=s4DeGP0a0TQ~1oe}6LW3h}9)59B~%y>xh@5YzS)O{-@dPT#Q z)&`PV#2(I=cg-|KoSjHC2U~(WJ(2};GW4o)p?E@0+1e^AnTw95H7{rl|FtHYL@oYd zib403ApaBn<4(&Lp)c3E;v3gS!_p;oBWsn;qoeM%h@1S7G-`F&rF%&yiy@bBS@%4R z3ec_TYl5Wp7w7Y5M~VA&otGRx?FmBiOxeGfcoNcz?$mfpiky!#05??_>3gNO#*9l3 z4`n_k-kJL6TTxIk{2<8jZ|s_Xz7Vp<U|<>Q=tYELhiB8Y+Rv6sp#T< z=I1YE<$6`fwS&fDLZ#%F4oEm;OCP;-jYf*h8E=(%hE-JygbMkM^Pitx!FpkA6dL-{ zq7#jVfP>ld?%v+S(^9{ibnE2YhilJT=P4lG!>{g65;;dlmrragBG@N;J~+f?n~IxF((H(nhzWM?cZek&`x&BUtF`_vm}s6tj0{-I=`8t_1<5@3h2%){DZ zhF|M&1#5fn^OBYVE;(?cKcu?QGc}*P6pEj+Iu&^_NTf- z?~awA%}+4`0lf-82D$GuF^P<7lCB5Z!~-FAwQ*6Okg#~d^pjW*vQXRQYq7%7m}a2ZF!#M0tQUi-)^_Tpu@bi&KncF z-n>qCU~V?OKc=1|?Eah9vsd?5XHW13OA9YuJ7IAJLTqd}DcKVb#_xj~JE=1ZC-C1u z&FshjY;*~?$Mu?XBB)H-*9`MJ z*+=V$qVo1X17l++rHGWUtzJn6(bs9j!ti6GhET!pBDw@9qL~`*bs`~{WV-t?2<73?Q59(josX`k?*pPG->0D z-mi{h64*%q^AK~ar6zjaHG$kb>vmOhld*6NrN4qi+xcErJ zS6?R5Fx7GB5YEH!^P=uig`xC?jEq$eiKdyC=PzEv+__Z6WwHKxuW%|r=|W1srjkl9 ztG^a(bq4hwS5WzP#oQw2*}vSx zYSZ+W8d! zD%E+VcRrn|vkArB)zQ?1@xR;wLsIDBS0`YF>*MsKJX6ETz< z=x+OZlRd5<|H1sWDYH0xYAJ(im1y=LXjoTxINehyXJUz(P-KQhBmdvH^JD5k*lho? zYjR48ax6hbm1s5|A>nksoC6LnrbWGh-C_+tbD7=mhbAYCv6E1{?Ow*B!evT~_TK)r zAdmIuKTe+>QgvOIp5}bUQvEKXql})JWOcUgbyEHHmqMJ9cBsiQP4Vyf5`HvX@mtxP zGu`9*yO%^G@sB_JoBogK!~YW{OagK#zXh6Pil+Ml7+lNHs{}0iL;)Gsm+$2pUCT6y z=Z2`;_`Ny6pteXaZNnWG^A{dgJu~W=mq1$E9k-uF%@cdYx;GavOrIL{BVd2}s_9rb z3wP<_iSTX^Z@O@zX#@mJ{5AWFGVAfv5p-ZVutecr=7*@DySXAm;gpbiuvujM*7Ew{Nf61|%xsfw5?dn&qtn)FQN z2=6!$H~IQ*Y#1O?R(3wAt(`iL7^hHvQ|R^<<3D4yPutPiEtZ4H;(?kqo#O^F!#U}tNz%U-e#|V&(lQj+)@&FJ__Qn^Z@zhd)9L$U6ip(Qyw<%~{^&j_ z$uSJ!cJbNRazd#6yfX74IeFxfzGPSP^+!~6 zWJyqH=o0pEnvArjMDC?1_x2>p1!k?z2B37(PY8GLs>q5RHPbDU>C8p8QX|pLG_gx* z{vne)XDqg13>~bt;sH@zan+}zuxSm|-cd1Mgh(c>&*pA4dh|d})^dNNG!-)i>ayF< zSyu(pZPxU9tKrnTEnAf;-rPdvX|tWNVe0*P{HDMl_fr<=Y8K4xaC}0CkLc{V&+RBQ?`bDZ z?t+S>9~rgWl3Z#1(3N0>Yxwr%Pf9=X^!hleA>7xRm{pHim>Dfgu^U>skfkY?%p3XV ztAl6-x&_Y(#2#P3ly1+a414XM_o4F&ZNnrswDKyxxd>~WurUXJJ6?CHoP(AWe=~G| z^md~TWr--}~27O~I zopVhav%wC25pT|~@9UbMB{19Y0Oi*}@x~U^gcXv1~@7!yiHQU+ieB7V*AE;ifjWPP@)OVJ+`UfLQJoE^FDj39Cx!-s3F>>YjXs4^QS8?QGU{ER74)MHd#u^X0>%Nb-S9E z`abH7NVJ;ZjPz^Ent__an*gD(eKh7-THc$uf-Cfpc)|pLlJAr9_2@y9ma8sA(C5J) zoN`99PA?dIUr+ruIy`ob+Z8&Rfx00>@l+`PuyJgTye*%RalTpM{YRYv?iKXz@4dKm zHwDf2tOUnMmcsmy>~~|1C+BO)a>$aQ{<3R@Z4G9OSmTHUHAe02ogrN?@b@KWtH!Hs zz?_%p4rA>LvFf#5K?^_!Pu5zXGB$s7dkU3uYS+0~8ulI!nUah#RMODcwe3I3IIs2$gJ~Ihiu}q2TIyT)4^3Buv z=A72Lo;;FBR=n`_Tnr43eh^1o=X<7)h(1R%wH(<^5#Nx)?v!y$yyp~p_E_Jz)QxIt zk;m|U7;`@Yn+;5zjhw+~Wl?(Fe?b95Ld+_~UH2h(b;k<4Mk;vO7jUCoGLw%%e9aBDVgp%T!)X3mwsQ*+I+Z( z_z>yK#jh_Ha#`4X_L@BeO-r|Dt!t_B1ew~M5PnGOnQlE5L=w-XU{{p70#9ftIa;Gv zg6|5f+ex)gxs54Ljb3w@9!?sKPvE^xhAW^}r!IfF&%PuC(c)NsQT{uhE&lfd<4v~A z0MXTs5Lz>2<8X+&oYZ$T_>Sr-mA0EcQ1_J-yz0iSa5nr>ZH#w3<7Kh(?hR_L<<|`k zs`9?KKj$d_%&-QwrCX?`naO%WYNMrPW@%di?z5rT8!d=Ete~LP3SzbDeVX?#qu@WL zcVaYJ?W@+F;lo9t$Zk`**PqBCV%gs4mYcQB)H!?3$e{9F7+#HjEXn>kdT=w z_-L}k^wLsKpLV%mugP+hCf?L6E!>8HzLfGaRDQbx(d&iQ#`e^$PAzs*DnmgYxRCE{ zOhXxObV*Tj%shZ%5qxS#4o#HYnCw-V(!xLe^6w4~S{mjs%IhbLA zqwvaiRB0grzIaNDdxyCQZ7x%SPF*NB^eQCYkhdXNL8QeOZ8md{Om!^N+A6v_TczC) z+wk7{yD(TA+3GDzWh!mfd@?PAq$L94XCgjYJ}k?3yTj}bsQi3beIm`+M>m@l-+QN& z9TF_KHXyh2W~sHE-JE73SV=a;5H4T@+VKPZNOBL=2qk>^bnkpiNkJQcUip(}l?&wI z0dI)QaRGGsc>*&=j?PJMwp6ogB}3nHVKrVcr1^g2tG|vkSEdckOM1qE!ZH0DeJ$tCz~I0%#n+Ub8}!wW6D@eVsz9*NFc@YG~e$sgKC%sn(2iu z`Y40riI2<(i_In^`2|6~15uxf-HyPPEwg`q@Bw62!lJF zfAG#V0|N)C5L?@!Iatvp>6~ebB;~~fC$pb+PdAH?i>J@BfAjOMwa^F()&egt2fluT zg3e59Rj)t6Nv?p%7F;`8JBW(Fd8e#S2S_0RoMt4r@%RyW7y5NQ;Ld+?DQzb2%yCwjLmPEf_!$DYe)#gUT)^$ z>E)k28cd=Nq`*%V`s-jdu*_B|bpC^rPb@8pW8d+RP}HG^wbHLtJo@Jy=)JT00C98< z-R3OkERRX|0aI45wkq78u?<-!dW`xW1w zHg`P~K-_uKmuu|nJ zQMy73k2R|M!~$MhqTVuolYUIhCa%IS1ROa9CIn%FH-t5cg+KWDz8YA{^Z-ALL6HtM0Lxitu9PH8C=+M2&mGZZ92Yvvq?t z8&LoaOtp`=Fivfesvx-NfC#tU%QP#7i{G~g?-$D18zl=fbWEW%@iv?I2-N>bQ9^Nrw04==L@7&b_{K_ zM479u2j$~2*0t|R^;pN=t~~Rx@4D2;RJpl+CwsW(esnW0cEEb&efsgnv)2a5kPXsb zvEB^2OqnV$ga4SqBw)|@vn}i}repYskRU3!b4(h#*L(?h3>!@bd{W`XSKeE58NdekNdu_|82P**>>qNp@_n*j* z)zzCv{nBm;Y*()d3M>q9Rry+@%$A{vK1cr&sxrYD&WC@nStjRcXATX0yT7z*)E8pt zKS{es6Qq3Oh_Y#FHe`d6CXHH#OMb0U(Q0uH-?^m`&<|*2g24Sul(unO*p1I5{PIDM zMDWEkv+!kg;V7!I5o*G-#$i+rlMN7}J=0h9uyteBV3`-Dg#hez(*b7!#trf<7z)a(wB#`b#oXL!@Ds?br%yf_QDY7X|?#3!AC+r zzS>7HWPD)O64P&VEAq-<;qTC*e6C1#=n$-_>q3D-za4)_cXox3p57Pf@nsmTuC%(e z+e;Id*`<|6^y34Gh;UcWBlI&{C~4Fb!57XHHO8*9i0sCMdb;GQ)_vxj^z}Z6^Lvx` z?1)FxGxB-rjM)l;@CbwWMk5Mdgi%|v2!v5|8Z3VJaiMu|cCRgbc=UYlusW zw0?n7l;l(QUz)_i$-siHf~hl{3?(>RYa!-nw^EP1LP~DY=cRTzq%@ja@N6fG3xAUo z7q;w+x`)Ns+M|4Yj(IfH(tV6-3rhdPff{XE_g}LB)wtB$D+{%3>o8vy>p6m_=>kR< zDuwvD)ll0hvP)Y+rszwk!^ZJ9;%kHJ>Fmwj-F$NJ;n4`@aMh$1+uY!T>~^d>R4UcF zu!Z(Bf*ri>O#|WQyHpi$3Xn1o^gkEa5V;_poUAl8)1!S%#K!(OuLKIIFxU*#=(=bL z^eE7_A2TKD@P5$zy*1`Yd1_!=@j1~>|5^8{vNS?%9QBtb={Af(K+t3;vsJKP{t~n9 zv%Y#hSjO|kv&uCDb<4t+BKnpL}W zC3akjnc&>VBz0zbaa;YE9gmWxJ3Uptwet_PRlYglNL=!-HRw1vILy+t{M~1%u#cFD zDMmb_Qq?#qgtTD30*Ke4^S*1abJ>r#2@==)3_(ffhP{R`0P5{R{P5@~FL1dx7rUN5 z2s7<$w!`z2S91GU%&rjw-2`>*be^YN2oa6ALsemAA-b~_L>72|C&GRtvi)?HIB;TW zYenpFU0xS+B&3`N&vjShW;wyaqS~&@5O~ub0))=B<8Dip)R^B&w44Js$@VvT|Qv3$HMbFdsV}Hsx)3Ef=!anAJ!$1D-cHoo)G67qWP zXLMJ~&W{aGwwqF4JI%e(1yns3WuYBo;=6hezY3R@ihemuHC)qVd)bV;xntVws=;e( zB;l%O#rq4q+-6=L;_g#MdQZM-oSLRq+=i>4{ zm_fss_fK*w3Z%l9$O;0P{V|3W7>w4GA=i$5vFLB>Nh$VuMK8zh6ZZ2Iw=cbvS3tZPJ17sUK1U@yO3l{1`l5KmhYM(qbN;q9M0eV69#`O~r+QgHK*_ zTp~B0?t0DKle5nwCSUuU9J^OnW&(@IhtEmfHcjYd>iqFiL#_%2)jZ5-J2-Tw&qM1R*lf$RG^i?h=|BU_og%Dj^U0GBF(vB&#{uh14Y`GFtGU zB_oBg#nm0!_l=pBp8lgW5}*9duQ9gfutpdRw7qD9WYpJfwX(&Ukk=)-)rlKl-a8hT zEg0(e)X14y?#5LgqPU6=a*8NP?T&pkB~-U-AGC|tn*_vp<5crOGFOeZ29-o@v?Bz~hYj@bcbGx57L1K3sU5u5Gu|oc4s_gS5d1BLPY^s(t zGG8*~Tomz%V)}!WC=I<4c+FUKTMx6_aJy~~*bxt_`yfAm9RFx3mf!BMfznJbv-@ZELr<1SB<}aM>8B4-Pu+cglVFgmWPcEea zP6ogYDUfodr>&Kum8IgsD5B^b8c9Z=fIzG-C3JW*iu`QN$Fd?7Fph&L6+c(%BG-21 z;I4>0!v%GTq4anQC^h%zA32@2y5BMF6Sw|;0=d={@1HSM>WZR-Ti-mD&&Ura;bESBdcB){jjl?>0S;ORqAZK8kw#g!=}H#C$zPE|7ay{VPD?Ay9*pEB9coEKp~$0 z9m6&BX6gQ&ijfB!TDPf#E1ao>CP#8<5Wt3lEg?*`J0#Z7shE5opu`3<{$DuPw*r|1}}+GKBwKN#D0#rf~m9(GzaVe%KZ&Q1fx zcJCO{3X&4iJ>zQsz#X}(4T?(|JlAs}mW(GLaA*zwKy-@pak)c|zICiOx7I?XDZ|)7mI*&lE6A3lHAM0d?|KUG17!b-&;W@3My4%co+$ zJC+gG+ly)MvJBog67=?Ca8bz$OSbdfF}U_7>%6~B+Wjjwvdr)AT zUHob|t@5p*@AF=LAq~x9GZZ&xz2&4`yMOuvY~on@wJ-jYvHtyDiuWi`GqYmgM}pz zzw6K5u}tO-`(d)e%iHC&;lQ4eXaib*2a-{z7hc3@%4f+oQ8z)CF|4H7&`M#GinudLQ<#R~1;k}u7;h#hkse=rNv zKl?1n1tuY=SUXW_L%L5aj+>GbG8U=?Q)5)Nx_ZP~^mPsWIWaY&#b$WjG_^4EMV+k$= zFOYfcJ9;l>X}3(;%&W}5HpOiavhnHS%T&VclG)JI7CbwAdxA4XuHf3u)PGa!v{Omh zKar)S5k}kx59&|;nlgq5a)`hmgnjx%t&}S&{cV3DnD4v($oE+iX>~XDNCK?vjsA+$ zOG$OG8iJhNwXRkgRl;91LK)u7gd=M96r2l;M;+LryglxmZX)d}(5$z32Z${;c}e7dOeS%qw5g znoL3Wiv2{eukrMVse0Bf5%1)bUT-Zrvz#O$=yP(%He&-cxOK!nL!#8M4X7>UKqKfd znDFmR}<3>S+Fp(a!TkgGg_q$%0PX(#rr)>9?9a@F9>1c%`-L~8p zMCJCB`>ty=@PaZ!1vOf@5wiw@aGpA)WJg~lk8;7l2W`wW)uhWSV6LYeV#?j?*g`hL z{)sHsBu4e4M?pESoJ0Y`88OySG}q_=Nr#cn_vp`+!gq6esaL(&a^lCuJBQ&aVxPo) z(kAY;a=*|LY+G;tNt}W9ZfF83y0}q9H0qKI%%Gd;*%CV98RLc%WJC=`Zfw;PD3{Dc zA``7lb+H7DdZ;p6KNhdF-q*P8_Vi&@JB-Y~6JA`PY{;lA$U zu$fOg$G;u^Qu#J*&_f6*^@{4JpuQGwtE0OF)D#JK;$a*Xt@Wx=VB0MR>csp-E7wde!`Or}lMb`d@vQr+&#e6n@-J>7~@-7v0})XJ&fC5W)nHW#B_JZ9tImKZ=6K zKR+pb<*GlEu`Hx#e0rbI_?L7whQm!P`!8MayYh52gV|G#E55Q_LAV<;S`dVnUgYym zrh(l0*_hkxBdSUE0F+Cq{+o7llJ$xa3B-Eq@85By+?m%Q>+=a;{qH{5#xZ4jAjGm#*sha9gECz}y6K*eiz$OS_Zf~Vaz)J2x>r$8^xMvNY(`=! zj0|{5(TMZ+f?6vf55fqBgU%E~wd_X0tlsq7&T8*DN}krVDnib1}lsepfkUmr!zG?q?#rEs( zx|R0H#l6(%`pgu(1F`Q6_y9=ea!O<+O*O}%?L?U(awBz?>uJ83>&OC@2LV#c4Gg*e z#P$AF&>WtLFTJVhW^XG&({2=t zvPYVg;(wr~VgFoE^3F`eY1SFZ(Fa~8ZLDvq<>4wPc*uI;6q4@%;u1t=5fan6k$BIZ zaGt0m!-QHk@Vaum-0MJ1NJhOPbMMkzTx5heH(HvE!5?GmBslh)>6-iViEzcZ!e^g} zru`A1_bA+6IT9%+^oWXMU;j#UV4qb&{4ft^_Mh|e?@)D&Oczt)PF!#OE> zuhp{z+RJsB()sda**;}i|2&k5|J=V@)uw)0qr`{@kl^JBY-6!Zla&$C7f(K7qQ5s% zm-(q@2eM7~q^|YF@N&ee%_m%x%(_?J|bKa%$XZEGFGXIP{hH;BCR^d_g(T#h>s{PsF=J~kh^LFso( zasCv0V^N(AFjcLF8+mc4j>7JR)itNrx|L*B3Fi(a4AAQMTe4Nt_i#7s79Y;c3Mcxi zdCy=UasF;gF*bYIuP%4y7VWK9w<-L^I&#A0CdYVrrd7ug0%J`uMLcyCM7Yb_Dw>bn zzlT3<_XP)yU3SW}eEF@fbE@%l=aIDMjcl$Y%+=t=jqu78>#k>tWkqmrx`VKH0%8wt zE}Xf_lt)bg@A)(iynWU^T}i+pa5t(PC%x+No@dY)@3_z^0$_x-xjlO=E8EXnDdtdw zg?2coij9cC_IIZo-Lagc)qp6VnibABx}-BlGW-mWzF=B;X6LG%%ke4Ieo^Hv79rFf zFsnt0pVwCh-6{EZXvVtfK(bi;)YeA-M7{>e;)Y(4fMfI3rU;Hvq`2F%y;7hmISNg& zEN3%-FvQPM+}B3t_c~;U4AS{Cv&F9E@1ec*a4Z{-nu)yZL6C@1HkuYo-Gz zq-dm8wmf<9Bei-;{vF)6(TOcU0aA*%=U9Qi78#aVi(HoEN7ZqGnCvHRCxM|018?$^X8E`i!Lic;-UM;2(^gkx(;JY+%ccyR$iu;o3yK zg*q8DpBK;O3Ff{`)S3*4_{S%NPHHID-qS>SUww2CAQ^eA`!aieC(uicFM->U7{NUp zsM{hYh0o=TJ2XTcHumPqTsSiZytwG4po#9e<1Plwgf+KCxpnGN@rg#rFWjdwxAE>j zkMAi%P>PBPwQkV<0QFcJd+{J7&B>_H`l{P#N#fnc-zGgLVV)jA24{ma+-X)(=MLwr zb=6d>OQdFdatoK+2e^%y#l10TW}D*sXt@ur-&uvt2nVOto73E`2L>_Rs4UqWXkH~G zl95#Cj+;Sg8FdA}c*T&y_A77KnK@a50@b0paQM`aqQQ6h$H_sS&u`kSBMY1L1_7ze ztWgHp=Nm&~Xk+x9{*bVQ1Hk!2d8_8jRlb2}yFfN;fdcbr2T89U%NNF|b;E;H`KqUr zzed!N*F-h7V1dpsx0yj^n%?9V+erO~P-5*f!Gvv%`vt!>wtzIg*@W%u0_GPhR&3pJ zSC>T?0ZxL!MW+;XHgC!)j4ykq#9mc`+XB@FcgljCx9>L)hB!{&fjDQKY~$V4VZ_hi z?ms~J0Yt(Hf|THzop9p*YKB}vAB^1v65Ll=t`ZLqxR}RK1w4vJPY)qK(qs<#M$7Qq zrT#L31XI2)lfChbY?+<>I=!^VDVM;5Zr{){ZC$Hj7d zYdP3&KX4>k&nITLj-`O!(bWm=3f}MJ9lEYbH}*+Dju=XfAbtAuNo45cz9=2cKohD?72J3ufM!4M+O6|V^HT;4K3%Q`@XHOrY z&Y)V;?hozJ<8!B`xb&RaZL)CL?qFwS+3?yKU;-SBT98hpm5xbeAa>@eAt2$JN4Gaj zu|SjU#U^-?8q@Q@j}(0$E>YJf1wFcHcnMo7Kp*?dzi189%%X@V8S+vhiDW zD#u{2q*U1UQA743{+Z}8_E{smD=b1Q7pI&~-yItiOc1sGXaSlI?{byTAuF=BJS7!b z>MNbkevXo~=G?45{$Rxt9akExrUL@|K~r3^&@V;A1@MX4c|SSGHXmX4f&{)96Z+oEjha zdZsN)J4}{}_Jyl-fv7jf!tzK9p9v5_qnnqYMk)w?2n$f_3H#LCb7=(C%zN@7s+?m% zRn};?0|5p`ELM9~C%ZUAz5NOmYU)x%Z)LQH>UpSQA$l)Kmd>`n04-}-ZQ~)l*-C&w zPTLCCjPreHy7SwWcHzJxL{3a$Yq8l8N&KTxXv}Bh^JQ!8+Pm&XDKhC#{b8z1fc`2= zmTMPXGFE})jqOfW2iXt0S-QW^px@2IgMG&%mRvdgAM(^IPSn95bNCWBweLS7796tN zzO80AcV^Rk;|@A-AW(0=hr87R6exBZPN@#8jYp>X{$6P<`cO-QTX)mA@@marD$W;$ zr>}eOrhD~C|MWzUn;2c%v(LnNOm9d|*ny|fne~l=wIEUQj$2jcIVjLfz;r4moB3hR z)K#qs2SnFVQK#%tfgw+6V>;inru`|3^5)0_SugZi{OK3)^#=>vAYoEwBxbx4de|4P z;R<0tWBe#7iz3Hs()IDxo=dtPFoO_!J4Q{V8N}FH% z3PwAMXhvNpxhm?PpifZJ`R{h!84vQ+>(~f+ePsGN4muidT(f{OrC_Tf$M1n+hRNRa9BEgso6v32TA8w2Mc0J9*5avRYu0@G<;ROMrWCePpP3Ev z%#Ohx+gQ z((+*dWDO-RizK9Pk>SW`)kVR-|Ew*9ABlBYsE@btP$NW|8%fhkpPM zN9r`jrSxE2YDyVgw+$4eG>C%=jE6`*^!QH17Vcod3il8f#Ar$DLl7)ELLw=#R2WKD zD+0uzux82q2}{T_tS}hov7!|9wL0PnB$zo0N`BtRv8%LaME;tLGrEe&#FxG?nEbPV z7MBtFI`?>+34FXc9BWrGbnx&5??~&;F7?W`iDZ%9~ z-im02G0Bl8JGc<@M}F@f*dJu|`bjd8{TAQeKd)jl$!gn!?^tdx0{-=w()SW0e-m~)e(InPVL1c{Iy^B)N z)i}iH3KekY!y7~rEef2`m&;X^~Zvg)Nlz*WaF8}MzYnm2E!-ujVjy9&G zjE~*dn~uuyY3M=45DTpM0!ng_pucFo5u(s{SF9v4c`dt)%p~x&P1!Hf*{~$f7eYF6 zH@0(}$<~$~{sGH=hn#pI_gy7KZtzrx!3Ev9uf8P60vNBDEdC?-!Q2p0^h-j?Aepm!G?K%)0eP*b|!%0`YxtLO1EN*ur;2atw~T`#m=QPk z&CLDWJ(ZHJi%GRc(MZ4hdL<-~lckY^6HVtUR=+It@5POu!!4cIlDkB`LDCd1a46XL@0iraZEvsVnsb#2w)DB_Cu)4S6~>f6hZuIW4O$`H9u}OU@h}LxK?%M$wiM)u$JZNl=X*%DO+@(W_Q; zn6c@03P!gxNIs(BVfpLt#NPT_t+oxzt>_NL{5Mtm-C;)l z!vpVgMgHd(Rn8FC%lU(jy7EK+1Yr}tUw}fTkmbd%1EUPjtH{oM^EoS33l=-4GP@;^ zS=&c;5S6=k))%adnb*?-SKj4K<#Mc7_r|`HF-GLU((lNhfy0TFD}jY2%D;2kPutCS zA;4{}H!Td;mi_Ob%G1a!9jsscg0Z@?(@V{dTmUJD#`^Rw$n7&r-KlUV=bT+l?O?q( zPH>w_Kox_JJ*3aIl#B)BIu{WY#Ysvdjx-f3`d4k?>QUa9t~oio7!$o2*`=jtG#26C(~ z%rWB7ygw9%KULs7ive^*Oa!t{xi{o!r8KY?t3lhk0NRf0HhiFU``Gr_3v-dEXOlp_ zIxC;;5BnxvK;7Vc)(KF(1!VUAtQPy+bpOY537Oxb2$HvK#PJt(=08v_6@mT}*Fs+rMYqU_H`0Tpcioaw%=_oB!9ihu z=%2k}QBjhcJ>>JU&5gJ9C#Ci3LB(vbouu9u&#A?IR_@LnwhlR;kj5wPiPGqKJGSJm z1cFqpi%+T$N6$upaB1S8c8b`}Zot^1H^m=V7Y~zc)7wlzyt#8}RmX`vu`4I%O6{?1 zMy8|ry^uZkRIENze`*J>L30Pu{g3c!kMR z8Y=ZS&y(=XJ)Zen^yg*MWt=N*95WvHCw4iEsRHdxj}@I2WSZAn#=<4nbB?dEg$vrk z+C@1p)i|$S^=PDHC1Kan)*pK>2ua2XqiRLr{tPZ$?Y6B^7JYE9uu>nH|8;)#76b07 z&&K67Q1WB90h-KFc8r~j@{-7z`DO**T-2Uq-f@?L&5}u(9>2ugU?4oWiUFRS-cy?? ztXL@F9Xzj7#xzN+uA(V8c0ZJ=^7UUVbQ%xy0v4~wQBBi-hWr|HF7V8`k^`qaUbI3f zvyW19zmT`864Jduw|r{wek7}|Y?EAQ@JgNRCAKUFl|>ro1fbNOiYTAi@aw=3bYuor z*fIrsuMp8h$mB4d?GMVU0*8jKiDeluq9BpqWc7CQ&s2)c^F>q8#3v7}JvsIP6??w4 zPWdR$S(P1=lZAu4GDkb1NsK{Vr`JsnYJMkt=6W0=)z*jZN679ec+&8w&(D;>Or2JB zg5TYKx!`rR3PdlO*GMAs4pyA+*RUzTevz~lt&!B23Mtz^wk^k-ex}&Ft=kZLkmR&p zk_}CXvDi2KnCsOX>&#=hLZ1A9WODI>h1iiMcY^1GU1@Kam~uGCuA0){02B>qZsxqd z`$wbpLPJ}BJo^q0ie(H$eMg=vnRA`h_&{2HjQQc8V4E8)hJ?Yp-IS%@1=>qsNv;}5 zP4*^S2U&aETv>jZddFx;buVrDS88eFm<{C{@Gk!TP#xf3k$xl4?4BCL+61 zs=B-QUBeiZr?}?vH;wpo`H-OH@{hix&q%y@HgF-kk?No_0%p+6aC13PmmeQ^TxrX# zw<>S>_cSb&sPJE*cue+*%$V1@;gB@69ldQd8AWobcnY)Iu%7Wy@IRsZi}*jLqCnTO zgXRWL=1PGht527q#d!Pb{D)quf=7}6MC~sTLu~)uBK1Gy=>L_=kNlsH@c-}9f77ag ze-{;^or7D$e)QUWiPW4_(ptrn{?*cEQ>_hm-K>vIKQrCjt_(Ze=5jGHa$rG3!E9cK z;Fq(`;Gh7|z>)3H*7~lX+JcGSmdt{!x51v)**gd}Q-dF_@*(Jwfq}$HPD)-YZ4bIb z|8m*M>cQ!=(aLgeA{QEhX7qDVQoym%9l-b!nBW7N-dZ>opO-%i)fq;(Be??XUplY9 zzBw)U$gjgzS@hC$G8pTxu?nE1vMvbbUCt$Pq(9)e{=U^ajJ|gPujlJLN7NkZaV-4x zdSxl@Vs$KC@M>}5+<~*HUM({m(Te}+GR(r-RBEN&C+<1YYkL z+TQAGWqB52PWLbBF=x@e6^kzr+u_)$x9`JlPI>)f7IZ0(4NRoB2Qp>^cKfAJ4#R6> zZ-||BA_jwm`muX8n(hFcw#&ZnBgKxT6|i-)hky(gFdsy%&=e{7YNpbt8N;RS7f<{W z^f5jWfC1#5KnB*Rx{n~cakJ%%c>DI~dJ*HcXw;j_BUX)RRa(E9h0?Jzh-G>4zLt%2 zoLs%p`WeycR0t16nxDO(?tKeOfRit?PWCs~Mb8e$*wIH zPB)hoJ~)#i%{QEpf>@{iuD`%2RA8(mDSzm@^C#Zqki@NkAcdmI-((!)m4bOUZ*WIQ zjMkbH`HG&(+sasG)?a-H5y{C8HyJVNRq!V#rAHy1Z0#YM3tN|e;@$121JK!0?}Olv zgS{piFtO-MW-e&O*;7cI-iRa9sSWftJ{nTcKaq?f4%QUe{7ug6qFva8F zNJh+Q`A@1h6^MGdkdm~bHbhPhkN-qT8(K(Rsd%gEv`@@0nck11+1oq@Ec>oC{? zL6Jx(wAL8rnOf5wVqaT*5Vs)Ahiy7<=pf4qkuuZ&3L;<0xQTsZY&Q zy<=mT%mk0dUTTT;U+=$%yb`R46)(Ky$i#ooMiqL=IA9Hp85(-EW#eVN`Nk{r|QKn;SH9}|+MH*fK4Qba9x9udw?ZFa@RlBPGX4NPgh;TT~q z*ETPyA9n3**P;X}8*?fw-DV?kC(6=#5(UXw^#AvDkxuuOYUUx26ScK`wCT>W9pWBi z-N91KdwqF;r5ln)*q_X$I^RQr3g9_{V$e715SF*KWLNc6HvM6DIAao1se22;5!w*5 zjK;%fQ|Tk+dNLZaP$Mn1YU5s5#o3kJ);ub4aoeu9H9tqM5kKi#cvd%;<+Z0dcbDB` zCJ)8d%wWyYc{Yi0{RJGaR3IOGz9CV|-MMI?7(|(!(*CqcFe2HC<|6ou=+0|Mt?_w6pZMjoUG)B>Wn*A#RBLnB#hHvYV%Q0{uVHs~yF9a3%pXfG6bLMq z(!vlu1}$ATU})>N78$Plf%oo7A2aSBHv6)0IGQ>*la|1GhA+SKwL2=iak|?W&nHs5 zO3!wG4!reHBZ@r6m<>&r-rwRO#BkntRT7k2^8A)W-s-A+CZ##&QXF&JD=^H9!6&F> zf4F_}Lt$8`A}xnKZ(IC;vO>63abmPvmVXj6|Hm{hA=#8P>kB)9Kom;0Q=k9vL+7WK zQaW7L`RpRzsgL>OQP0t47uVkE>hlJ{?*s=Ef(ur*XK(y2a<0`7)^DS3^cPZ`6hLzUnd;1c(5vONq|lBq;PzC5%wVk zlOC#H<=bBwD*=^Zg$|;1cxag~UQo3QdRql*w1dIV={Xcp(m+I|83~^*1daY`q5yaR z)plbH-uny(>vfy+>Ga`?EoZ4v!kL$As~7(&-S=Myc+O=SBg;m>`FOBzOqntcCF z?uwBy$Ge}*3&rD|x|v*Vob=;Q-h{?Ny+4~(hRg3nCCKHL@WFSkNvht+=Bzzj)dpk^ zFH8wGs*8&kRCFUK#*W=}f;>VAm7V!d6BaO>2fdb2YgH4GdnXOMV;WwWu0&#kV<#iv z%flPusv~HDceb-TZi@@66}XsEp9`n4c2-5J>;kWCLS%0*Mi&|{phE%~Si7@>y!{Ko zd2;^8Z!Pv6j>P<#6SD2qY?K&|#%pUVi;0h4J`gEe@W;~H*5?EV1SfV0mt}RPTK`BB z?fVc_ZXu(tMsMA_R2j4NdPh)qQN61=#?1-N7}Q;nLp)lFrYX^kM{|$NHk&Yz{s|fu z0q&&TIfbn?#(yU=C&Zt$)<@wHI1Fod;~d^hS**T+_vXopmVN$Qc7Qfo_=;oy=Ik=W z5M##0V~BW6j`_EqRxhm|1Di{)ryOlsf#eP@$LTXWLD#e;pS>g^^eRbk%d@}>-3++P zrAPuATDXEII5zbR?jX)|(>Nn*N~V3CcPw6)-Mh+aY=?ulaWwn0o0&Td^~+9kYGmy( zs`j|sK+x))iB14uAZl;7aII=Md~Ym!^Z__7iGEX1&on1NGQF?$Y#EWo6j6ynaSd$C zpQ=xAz4>QuAcmrJplyC4o1`#u@0oye*2@SpSL}A z^I$|RUvvD6NLmwu?D1UEAF!<<Dw`wFR+C30-$ZdrH5lFiq*+PV0wzw8{#vDwNPHG3oz+j+ zI;>vse*fRfF<`!y4SMBuV3kP>J$_qa`yHw9eGpoL@CjOchPFWtkvI#F<2TQKL9~(P zck12DwjveoBrdPR(YC5u#msD@lYLvG`d4v*G`u0Cm7_LhebfX2cIK;VqP~y%vbEk` z3TpIQGb+_Z71p@SErP;&EnOjs;nglO$3+%0?!~+Mg2n*?pm$os)1l|0&vr%?EHDQljJhBZikSG!LM*1_qu#>R+ zd3TtlK3_V@QEP2KueKsuTYYxnOjN66HhE1MF5M3sWu46Ol5c=fuBYeT4KC`z_cG1s zL0<>=ZeTzrfH+GxBnPZSZ0cQSc{;SjA>OoriFK0Rko)5ZYCvxSN{S1B#Q~w6@Wrmg z;X!LKM#D(%|J~BC+s<-7pr4=t$RA}BWOKOsBveD>+Oon7=e3%>V^I5W+<|Q2DQmRc zl7nn}FKxE8DzBbpKAvFf(-jL`pJKEU94(A7P&GAKMtX_$h6Zi#h!Zqpu+23!Pk0zirj)VK}rZAB-b?xv2@a-&ta9S`k@f8UF&t&VnVjwA(#?*<>YF;NJ8_LvxvYRZPPyLFw>jGk6qef566iJjeE5PQE$Kll#-ZZifda3_;vTD+^M4%=A zL0_yMN0-#~EhaK8J1|Hd9xjrww`mOJ;iAzFV`Slh=k`q;A2*vRf@lU&R8J7or(#Rt zc|m|f@)McJ6sF-)vnSD@(3V6v%3fE?sdoL~ zV}0L!bNZ?s4)LR522e>bh9LDTO{S*-wpg-<)AK?04>1ASpCV{D7=mkgqfvjIhKIbi z@1NlE7bvF9XJz!B1&uA+CvSv)`Uoc8lo3Jxid=U#9X}?BUAyPXB}og^Uc1B4>TNe) z%Xxq&wH#-d{Jkxv`K4HrMO&~oyB$Z?(>r2Iq%6Gu*iRp!g-Psu2$ z_q-ZxnrCu#2aCR1t;tp9dZaACIPK-;(4>&*27@*ye38C5~tbjTQ}MB>U_WXa$+>HS+B+q{oPCK$h@jMQ|(j zY@`uAs4*M9B?Iz57xg*+Q`t@#ZpqusuH51ldv0p8`H)2FZ)BTOlBT`V7)7B*E+HdL zwY6v?N6EFhsd*EVmFir^Ck2e`q>2)<00JHSF1}Z+N6lzV8vZ}*-BnOs-P$jB5&{7d zBm~#sE(>>ecbDL<3)eu9;O-8=-Q67)?(QtyZLyH9cYkN^{?6&sebG04(^vCiRE?T7 z>zO)!|7V`*aM;~h%t`qBp}<*P!G2n+!!6V1aZtQr2M$A*}mRjMWz-Yyxn zt$WdT1r^$Uu{Ho{@}R7r=SB7IYZ^}H`A>K1YV=nZe9197!}X@Ke9>5|O7etEL|Dil zGO+8fZ9iR0*Xpg*X7kUWsNGg6g%cMBoXvIDT7u0;cTHOTOfhep2X;29C^mKWOp4NL zBAC4Hjc%f!+D%H#Hkuk51bQb;9^E<%xMuQw2}vK%CN{(kPFhQuk=p9t=+~XXMh3|9 zCtsuA<4A{o7LzmvdXMWLo4jG44+cEwWKo(J3LU*F_34ilJi@ z_AtJ~ifJEncY0BLhBf{Z!xj3TJh$)RvE%nB*5J`>^?Pk_gW!*<@XSwpqYQU>;< z{beOag7v}DWpsgI&U>_SO(N1CJlRJ!-r6mu#4Go!GaYp*KE!Rlj96|?wwY~z&MiJ7 zAjy-jjqus&qz6YfLRelMpEGOk#Tb*$R8O_*pm;OVh(Xy;!EhM`*H&A2s_m|$i(W(E z@!j#NBS^~=5HlwI?C875pW!%x_~{Wo!wfUC_`-A}dxMcWJ@;tCL*k$7+YZ|651)9O zY>jTSzYJuwrO6NrTZ(0yn3>0Wg0%{zbWgQ%CO6ur%c}6*uyU;{YB(cyV~{G|>Tq`O(>vZ~fXY%XUsu-0B0)XjmcQ~-DrlYBlW*q|E^lW7TmPxB ziHs&Ea`OHWiyNXfI7QCj*sP`7eOU@_=3gFbs~#FETnQgyCkyMBzHHp9v%z^QDxOuA zv%1X4z;8h6htPG$%a>PfetLHITei0E?jYsy2Hs{Gq8O1VAKBnatICtH&`yb`rwjd5 zLR!pLFU`%QT$%4U{>TOF+TJCY7XL=%dr7~5FZ@Z&#`1XgkA#h;PnO;ms8609_kPeP zJ!@-xaYA?5i6va_-YB`1+dg}8y^Tm{b9X@!9?j#{wPT%Ohv!#7%e15YVJr1WM_RDT zOvN-Yaf_!HlO%An_&}9|fZOFexM<=knrGl_yL*q$6%JaaPLtx1)RXAlm`fS-JvmQh zbN==#Ls3jRa}iE{-w1xj=qO0c!rmairxu`N!L(j^Zpmej$Z(lm_CJKfjG9}o_V>Zb zT)uPVe<6hg#;SAb{mnJIABXGQ#y1=|U$E0V->{fCD+6RbS(PSgO8@TUYp9A%4+fc- zVIHwl2uP3+N#KbqVf5r49!qj(>b!gq^X_ z49dh(lZ&?YzfV_8dK*^@Iat9!?62mMYRWqvs?W;bh#8RCqpq&_0zR@wfaEZNx3hQD zu-D&|J&E*lspx+MWim2y*{BD~uqkTK<@&15lg)Aa-fkMQ+gDl|GQ{d{Ngn!DH2_uf z?gm1|_m5d<)6%r5-C9j*Mh$b13c^NMnc{FIq#~0w7!@pA}hOn#f)htplXMI#l&!s0JhqENgVi9 z;>MGT7y=CqD2Ziu-gNBM_V&j8sW!S)I&?U0JU%(%qxaGpE-C+yeZR(0LFRFxlytls zAj%vr`ToW?Q$|+pf;+Wd6$-I`A&5?`ITvR?8lkz-8k%L?Ki6T}Oyvr|{;qL#uw31e z9RGWFHp!|!J!v}?36J72U?_O~Dky)($?E=75%GwBdl(68<+6C2*B(;0Jl=ppjzoih zDJyK3cW{bd`hdmhZ#e`IO{z<9xp_jC>P=E?J~a;d1GDeC&d1xT_5}i#cr~@gqmGO= zIZlHM_sxPsI?XHv2cM!NqIeZx|y3B1?u!Tc8xAGt}xCAncXIVwj*ukc?+dWq3gkG=xZ9JOOmKVfV zmoJ6nzzUT-z&ZZrb`Kc)wxejU*bcwU;0i3{^TwCrVNWX(HnhFwNn5dexu&FJYQo-C z1YmGB&>}nm;H15b^Uow>B@d?oUpBci3EO@6$9^1RH&!Kua463dsYFA2CIg=pq;U7(Z7o}3Zj=GY}Y7e?|CKh z=er%_zpP=Rib|X{rK6)M#(Te4-*}#sb+D>mzI7vlt&E9%wf2&P;VO$vdr?K@aK5$C zdT30N)Cv*J)%T(i%SYzX8W4Hft1hVZHjZWOrLG!&ndVY0R-hn#wEt-+HFR#_u%b$% z=$YbY>k(^`NE)T08piLJ_+!n&JdJ#f7#BvV_BJ8Y^nL| zkQFX2v^FM!u$TlLK*j}sy1cI)#!^eSujF)31}{w>8~O4yBXk-Z)B_UQ-$?Hj))*Jo z)z&>Uxe~y1m2^amp3(LPVJY?5sU=U)ZV%SBBF{gk;T;(G+4ftXUUxP!X`ZNM`;kc% zY|VO$FImq^_?f!z$PbiePVau&$2ou-TI;_kz$xhO?FQ&(bN*ZJ97SA;4z~DbFA6xO zH>+|OMQh9)yI6uIZkt&(!O&H0BS^=sACiFcc zkKWzCFUQJ@R-GP)gGKYs?teRy*ikn9TGq_tQk6#RVo}-clI~hO?75eUJ>@@-t*sdO z(gi%>b9g{njS(&B7XO}RFgQ|=+M50`f6uX~J(-RPAhh1mC`7to$G?_4m!Y;MfN~HC z+wsRfwsZ8fhr=o`E?7?Qn5DO3x}*K?=?n*jb3SE_`>WqqJLq(^JbUKZnN#IfFp0y$ z58LsaOr~Bea#3Tx>oRQQ4Cqh8&FpXuq!n;;9rr*)v&3Veoji6X0qvf0J9ie({E!47 z`>}794!|Dro0mzYOO$ET{zyh#*P9QoQ#__gVG#bOwTo9u$_bZ=gJIHhHOhA5iPPDp zYZ!r$JQ5{!A(H&sLSa5Uv-@A8M@^5}0XnCoGCt^JIh$j7kbY7+-@5^4c9&;r7L`M~ z8Wmnjh>NZA=W1Han!39Ff+Fk(ZfWwrctjyN;}qB*#86GJ`1)z?d0pFmV+>MY|MaPJ78A1+ ztL$zw0s~H1C_IZcYx=KLwM-IV%`SvLhM1h!{cyAO zah=z~!n~ccczrL=Oqd^tS1`qHQ231yS2iG=y+ozsM=;WwZsNA`WsdP_msXhzp~SFG zAHl}a&;nEaCnPK>jD*g}%EPWkEMrLa>jaH(2u zOtY@9cXj2LRJMuR#pdCdKGB}<`I;cmp}L9tIdnJCyBJm9dfj3xdXDcNAN%kS}#Qv-A)`S?#T%eOF!rtOJhRlN9pXRKO;mB3@Yd2NyL)Y2OZ|@ih zwItBG%JPOH-F_AfAa&p{KH>4bz4FTg@osdl4@fPX!#wlSc&JnTtGH^QSCk>ic2Az1 zJ2kj^KDMn*m4GW_$9}s5B#$+~t4g0$ttK=5SJtMi1+yqt)e6teK{6`w`x$@6i0bvq zS^6QQl8!|MX(9Le^pUIK{Um`GjG;TW=pb^>!iZ5@-`k}{GuE<#~t z88id`@%06eT&+mf?DGr0UFCARDerbKD7Vd&oy-prK;LfJPdkmsBQ zJul=89*dYzr1<(4^5b6d5Csyl0Q5J}nxd(&OdI>Xyhfna8nmUCeqfwKs|>ZcGV5Y= zdA(vaM{W@9#2!%IvLfitM|ZbnG-*6PdhUhky3kAY3GoinW2y1@ z;PUJpq{lzDPTZ{hME!T{mGuk6Sc2E9={?X>^b9bxxpNmy1KM<<(iDu&9U?SZ2&et} z=7=QAE4cmW0MC3nPq?U{FQ zpwkJZp&uxOD8>4k4psC|*P~3Zw0JH&m0`n!7)T4aJ)E%k+qRX3kDyX6ANFWhPSKU~ z_OS5RHb8$f=Qmv{fCpp&^vVI@lck!HoNH!Cz`B$0o{H%&_toCxir1vCq~eZJ1_CIW z-H~uwng&z!3)=xN#m(D8Q;|gN!94`SKy>UBztIM0meraGsa>Wm2_R6{qL~?0C<;8X zUW#!s7eTZ|KhTTQpTtk6)zFCJdq#|v$GPP$r*i2u;7)=W;%2-!7EMmEesEGvr|soK z%}(q~j#|3|IV0oNG)pb~B~dI|JW(}A1vJ_CB=M=S$nMr1XZkOGur0=&^kW#x_1<*A z7cpEIm6j%Jbk%sTY0rJsJGW^^!6QCsSJuc$M3G zRn1~}5szA1fuG~p;jcvfInPx3xSDTu{Z`R~z75)f)TVP6j+NMQ?@JWP9&OIJ(BpO_ zxz0)#pO96e<@bDw+NW=rIn&DKkiKl<*zEWecDDo)$F$8j?J7QD1T5S*%iZL1ZNREH27w;CQYrMWP=9xoP9RiP~65Q{nP4bXNOygCvgtWKU4 zR^43!A{Jm(JkidFccN^#50#lzSDt~okcB4>6Z5E^6`p`!bEl~+?=Um8`+OmFCidBr zHfd?vEbfD6x*)ykH|h;AXuyeA@K&&AfcWl=xK^t|2ZjB@DBBl6+a{2-3kNZf$*6{(8Uu! z)!Sl9Q|EF8?St`5iv7zhSk^B|ya{c0E0=5T`Ba;Yi#5=w`_sWS=Ete?pP(uEU5DJk z;&Iy|Z-+eS6aT=d8o7Zn>gp6skJD|czt5JR-x9-fCf~o(MpbYd>InIE45r{ z!G1>{Onana+R5M`dY9XJ|2(8)4<@)K*l!}%Mkmf*vZx~fM_`;*(mfi@#0SjRK$~O4 zUi?)Zz250KCeDRQvFXpG`QE%c5uD@@NP?vCWSXD>_o|zPN4GMOmg`SXYlt11`-3A5 zC8NDBo>vH(`H@o^vHA=WRXS2znw9TaJhagryq?JQYbh@olp192)r5Q7xc)>Xts5Lb z!7)Q1HSk$&;DnnnZTAZ6dzVdwMKq>=GEc`*gW3^ysIn`IMQ)C}uEmr~#kBg_V0@0G z%F(EQ%&yH)0_bt-fV{pQ2! z&yC+?YkV4lN&n=xJp?QD5W zLG|74Exrf^34-r4c^LZMWCE4oc;U;rlxQrLxW3a?Rn4a{e5x{mJ}}sB)udiuw&s$U ztmrP++L7UFG8s?q2Q-wbJvnVn|1>TQLmOz#^clz9*6cL0sbc|% zV#DEJH2i>E0V(~gUJ($wW@k8?vs53gTfT(*e2f09ImUHMx13@x1Xe7*nhR&Q-E2!^ zI8CX&g4W)ZUiNh-9L}mVLG}D>Pov?3(JPAq)lj;aCA#G5b9z%_yEoFIy_C_d?gtDJ zgg8!(LaXIm9Be;sg*#x|*eq7H47(!3=Nv_+DaM7I&hgirS~R#&yr>elcWOgD`B%-8 z5+}ceq3TbL+r>g}*$3fc8-kp5^h_D%Cz24)PA~DZ*$4jOJr!!LMm#guW`?IT*7+TA z3;l3w^~`C#iKiGy>6PBv>eqzYzL2pfn>?rqqXjxW;t%b~m{A^n!;^RM2Wp@XL{XX8 z(G2Qb^)lD~$_pI?rUA2y*P~Uxyug}pYG$hnj`2--6=J_~-eE?Jz2GleXyf)r*ISzz zG=cEyE3Rz;$ut|C2wU|5!KaMh<6#~s#;&Yqq>S%=HDcRQ$`#e$GBj0X1z%2B##crt zwdt^~M!lqKYDSy%CBw`T5$hpwsPoQsJ{bDA({GA>T zn~|tnkNlt3Kupx9H&G=P@7NLVQiftQkWS0hb|$;i#6Dr-!9v*bmAK__KNPxS$eich zucE8+hU06*OZD6Q8jgTi*--6n7xvIVm1(a=^VLK}YKMZn^*Z@G)$hyi>h9*53assf z)ELxTnVTCKss#;Xkgtyjoohv4VVJ)FH z3{j%uIy0#j02gNslQ>GBt?2?7AMok6}tEDtT8I2PP6T-)Ox?CkV2%8+6S@= zf!a=#@R$q&`sS<~-gFzomy{?LI{vT7o8DaqUWJO5`t=yW3tTS>FMXL1@0JAHeBvyX zQf?1;vMuMn^if+s)Vn23=AU=wB2kAYX&Vs3vC1WhTf28WjGD6EGlugY=k%;?3Olh= zLOyGu%uYrum7g05l5G&mLn6T{32BNaxjKu*_1QMS*Zz}x;U~9n<3$PlbXQ9;3T*s% zQKsv)S?℞N#BVpJZcpp4o2M@3j1UsJd2e*>_(zn{*n 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%0D|ntNJ-n;LAtaZtqKWusj^uWc=9)UCQInJP;{oxo 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

EF@hm6i9W&qCh!&xw`$AB1A?a<_C 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 diff --git a/internal/api/client/instance/instancepatch.go b/internal/api/client/instance/instancepatch.go index 64263caf6..5085399eb 100644 --- a/internal/api/client/instance/instancepatch.go +++ b/internal/api/client/instance/instancepatch.go @@ -175,6 +175,7 @@ func validateInstanceUpdate(form *apimodel.InstanceSettingsUpdateRequest) error form.ContactEmail == nil && form.ShortDescription == nil && form.Description == nil && + form.CustomCSS == nil && form.Terms == nil && form.Avatar == nil && form.AvatarDescription == nil && diff --git a/internal/api/model/instance.go b/internal/api/model/instance.go index 5232e8d66..d59424fa5 100644 --- a/internal/api/model/instance.go +++ b/internal/api/model/instance.go @@ -33,6 +33,8 @@ type InstanceSettingsUpdateRequest struct { ShortDescription *string `form:"short_description" json:"short_description" xml:"short_description"` // Longer description of the instance, max 5,000 chars. HTML formatting accepted. Description *string `form:"description" json:"description" xml:"description"` + // Custom CSS for the instance. + CustomCSS *string `form:"custom_css" json:"custom_css,omitempty" xml:"custom_css"` // Terms and conditions of the instance, max 5,000 chars. HTML formatting accepted. Terms *string `form:"terms" json:"terms" xml:"terms"` // Image to use as the instance thumbnail. diff --git a/internal/api/model/instancev1.go b/internal/api/model/instancev1.go index efa6d6faa..6dedd04cc 100644 --- a/internal/api/model/instancev1.go +++ b/internal/api/model/instancev1.go @@ -38,6 +38,8 @@ type InstanceV1 struct { // // This should be displayed on the 'about' page for an instance. Description string `json:"description"` + // Custom CSS for the instance. + CustomCSS string `json:"custom_css,omitempty"` // Raw (unparsed) version of description. DescriptionText string `json:"description_text,omitempty"` // A shorter description of the instance. diff --git a/internal/api/model/instancev2.go b/internal/api/model/instancev2.go index 8d6873497..982ed0c63 100644 --- a/internal/api/model/instancev2.go +++ b/internal/api/model/instancev2.go @@ -53,6 +53,8 @@ type InstanceV2 struct { Description string `json:"description"` // Raw (unparsed) version of description. DescriptionText string `json:"description_text,omitempty"` + // Instance Custom Css + CustomCSS string `json:"custom_css,omitempty"` // Basic anonymous usage data for this instance. Usage InstanceV2Usage `json:"usage"` // An image used to represent this instance. diff --git a/internal/db/bundb/migrations/20240924222938_add_instance_custom_css.go b/internal/db/bundb/migrations/20240924222938_add_instance_custom_css.go new file mode 100644 index 000000000..14231927a --- /dev/null +++ b/internal/db/bundb/migrations/20240924222938_add_instance_custom_css.go @@ -0,0 +1,44 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package migrations + +import ( + "context" + "strings" + + "github.com/uptrace/bun" +) + +func init() { + up := func(ctx context.Context, db *bun.DB) error { + _, err := db.ExecContext(ctx, "ALTER TABLE ? ADD COLUMN ? TEXT", bun.Ident("instances"), bun.Ident("custom_css")) + if err != nil && !(strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "duplicate column name") || strings.Contains(err.Error(), "SQLSTATE 42701")) { + return err + } + return nil + } + + down := func(ctx context.Context, db *bun.DB) error { + _, err := db.ExecContext(ctx, "ALTER TABLE ? DROP COLUMN ?", bun.Ident("instances"), bun.Ident("custom_css")) + return err + } + + if err := Migrations.Register(up, down); err != nil { + panic(err) + } +} diff --git a/internal/gtsmodel/instance.go b/internal/gtsmodel/instance.go index 027d8fba4..97c0268ce 100644 --- a/internal/gtsmodel/instance.go +++ b/internal/gtsmodel/instance.go @@ -34,6 +34,7 @@ type Instance struct { ShortDescriptionText string `bun:""` // Raw text version of short description (before parsing). Description string `bun:""` // Longer description of this instance. DescriptionText string `bun:""` // Raw text version of long description (before parsing). + CustomCSS string `bun:",nullzero"` // Custom CSS for the instance. Terms string `bun:""` // Terms and conditions of this instance. TermsText string `bun:""` // Raw text version of terms (before parsing). ContactEmail string `bun:""` // Contact email address for this instance diff --git a/internal/processing/instance.go b/internal/processing/instance.go index a9be6db1d..fab71b1de 100644 --- a/internal/processing/instance.go +++ b/internal/processing/instance.go @@ -227,6 +227,17 @@ func (p *Processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSe columns = append(columns, []string{"description", "description_text"}...) } + // validate & update site custom css if it's set on the form + if form.CustomCSS != nil { + customCSS := *form.CustomCSS + if err := validate.InstanceCustomCSS(customCSS); err != nil { + return nil, gtserror.NewErrorBadRequest(err, err.Error()) + } + + instance.CustomCSS = text.SanitizeToPlaintext(customCSS) + columns = append(columns, []string{"custom_css"}...) + } + // Validate & update site // terms if set on the form. if form.Terms != nil { diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 750d4eec4..fda59610b 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -1534,6 +1534,7 @@ func (c *Converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Ins Title: i.Title, Description: i.Description, DescriptionText: i.DescriptionText, + CustomCSS: i.CustomCSS, ShortDescription: i.ShortDescription, ShortDescriptionText: i.ShortDescriptionText, Email: i.ContactEmail, @@ -1674,6 +1675,7 @@ func (c *Converter) InstanceToAPIV2Instance(ctx context.Context, i *gtsmodel.Ins SourceURL: instanceSourceURL, Description: i.Description, DescriptionText: i.DescriptionText, + CustomCSS: i.CustomCSS, Usage: apimodel.InstanceV2Usage{}, // todo: not implemented Languages: config.GetInstanceLanguages().TagStrs(), Rules: c.InstanceRulesToAPIRules(i.Rules), diff --git a/internal/validate/formvalidation.go b/internal/validate/formvalidation.go index 207e8e05e..4de7636a5 100644 --- a/internal/validate/formvalidation.go +++ b/internal/validate/formvalidation.go @@ -189,6 +189,16 @@ func CustomCSS(customCSS string) error { return nil } +func InstanceCustomCSS(customCSS string) error { + + maximumCustomCSSLength := config.GetAccountsCustomCSSLength() + if length := len([]rune(customCSS)); length > maximumCustomCSSLength { + return fmt.Errorf("custom_css must be less than %d characters, but submitted custom_css was %d characters", maximumCustomCSSLength, length) + } + + return nil +} + // EmojiShortcode just runs the given shortcode through the regular expression // for emoji shortcodes, to figure out whether it's a valid shortcode, ie., 1-30 characters, // a-zA-Z, numbers, and underscores. diff --git a/internal/web/about.go b/internal/web/about.go index 2bc558962..843dda652 100644 --- a/internal/web/about.go +++ b/internal/web/about.go @@ -54,7 +54,7 @@ func (m *Module) aboutGETHandler(c *gin.Context) { Template: "about.tmpl", Instance: instance, OGMeta: apiutil.OGBase(instance), - Stylesheets: []string{cssAbout}, + Stylesheets: []string{cssAbout, instanceCustomCSSPath}, Extra: map[string]any{ "showStrap": true, "blocklistExposed": config.GetInstanceExposeSuspendedWeb(), diff --git a/internal/web/confirmemail.go b/internal/web/confirmemail.go index e512761f4..21028c6c4 100644 --- a/internal/web/confirmemail.go +++ b/internal/web/confirmemail.go @@ -127,8 +127,9 @@ func (m *Module) confirmEmailPOSTHandler(c *gin.Context) { // Serve page informing user that their // email address is now confirmed. page := apiutil.WebPage{ - Template: "confirmed_email.tmpl", - Instance: instance, + Template: "confirmed_email.tmpl", + Instance: instance, + Stylesheets: []string{instanceCustomCSSPath}, Extra: map[string]any{ "email": user.Email, "username": user.Account.Username, diff --git a/internal/web/customcss.go b/internal/web/customcss.go index b4072f2a7..36ae9de55 100644 --- a/internal/web/customcss.go +++ b/internal/web/customcss.go @@ -55,3 +55,22 @@ func (m *Module) customCSSGETHandler(c *gin.Context) { c.Header(cacheControlHeader, cacheControlNoCache) c.Data(http.StatusOK, textCSSUTF8, []byte(customCSS)) } + +func (m *Module) instanceCustomCSSGETHandler(c *gin.Context) { + + if _, err := apiutil.NegotiateAccept(c, apiutil.TextCSS); err != nil { + apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) + return + } + + instanceV1, errWithCode := m.processor.InstanceGetV1(c.Request.Context()) + if errWithCode != nil { + apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + instanceCustomCSS := instanceV1.CustomCSS + + c.Header(cacheControlHeader, cacheControlNoCache) + c.Data(http.StatusOK, textCSSUTF8, []byte(instanceCustomCSS)) +} diff --git a/internal/web/domain-blocklist.go b/internal/web/domain-blocklist.go index 5d631e0f7..7b6710049 100644 --- a/internal/web/domain-blocklist.go +++ b/internal/web/domain-blocklist.go @@ -67,7 +67,7 @@ func (m *Module) domainBlockListGETHandler(c *gin.Context) { Template: "domain-blocklist.tmpl", Instance: instance, OGMeta: apiutil.OGBase(instance), - Stylesheets: []string{cssFA}, + Stylesheets: []string{cssFA, instanceCustomCSSPath}, Javascript: []string{jsFrontend}, Extra: map[string]any{"blocklist": domainBlocks}, } diff --git a/internal/web/index.go b/internal/web/index.go index 25960cf7f..dd9d80561 100644 --- a/internal/web/index.go +++ b/internal/web/index.go @@ -59,7 +59,7 @@ func (m *Module) indexHandler(c *gin.Context) { Template: "index.tmpl", Instance: instance, OGMeta: apiutil.OGBase(instance), - Stylesheets: []string{cssAbout, cssIndex}, + Stylesheets: []string{cssAbout, cssIndex, instanceCustomCSSPath}, Extra: map[string]any{"showStrap": true}, } diff --git a/internal/web/profile.go b/internal/web/profile.go index 60157fd19..741dc2a83 100644 --- a/internal/web/profile.go +++ b/internal/web/profile.go @@ -132,7 +132,7 @@ func (m *Module) profileGETHandler(c *gin.Context) { } // Prepare stylesheets for profile. - stylesheets := make([]string, 0, 6) + stylesheets := make([]string, 0, 7) // Basic profile stylesheets. stylesheets = append( @@ -142,6 +142,7 @@ func (m *Module) profileGETHandler(c *gin.Context) { cssStatus, cssThread, cssProfile, + instanceCustomCSSPath, }..., ) diff --git a/internal/web/settings-panel.go b/internal/web/settings-panel.go index ec8166e95..41cd8666e 100644 --- a/internal/web/settings-panel.go +++ b/internal/web/settings-panel.go @@ -53,6 +53,7 @@ func (m *Module) SettingsPanelHandler(c *gin.Context) { cssProfile, // Used for rendering stub/fake profiles. cssStatus, // Used for rendering stub/fake statuses. cssSettings, + instanceCustomCSSPath, }, Javascript: []string{jsSettings}, } diff --git a/internal/web/signup.go b/internal/web/signup.go index a943f3680..64b9f4e2d 100644 --- a/internal/web/signup.go +++ b/internal/web/signup.go @@ -126,9 +126,10 @@ func (m *Module) signupPOSTHandler(c *gin.Context) { // Serve a page informing the // user that they've signed up. page := apiutil.WebPage{ - Template: "signed-up.tmpl", - Instance: instance, - OGMeta: apiutil.OGBase(instance), + Template: "signed-up.tmpl", + Instance: instance, + Stylesheets: []string{instanceCustomCSSPath}, + OGMeta: apiutil.OGBase(instance), Extra: map[string]any{ "email": user.UnconfirmedEmail, "username": user.Account.Username, diff --git a/internal/web/tag.go b/internal/web/tag.go index 5c3cd31a6..423000f99 100644 --- a/internal/web/tag.go +++ b/internal/web/tag.go @@ -59,7 +59,7 @@ func (m *Module) tagGETHandler(c *gin.Context) { Template: "tag.tmpl", Instance: instance, OGMeta: apiutil.OGBase(instance), - Stylesheets: []string{cssFA, cssThread, cssTag}, + Stylesheets: []string{cssFA, cssThread, cssTag, instanceCustomCSSPath}, Extra: map[string]any{"tagName": tagName}, } diff --git a/internal/web/thread.go b/internal/web/thread.go index d3ba6ea5e..246c05b97 100644 --- a/internal/web/thread.go +++ b/internal/web/thread.go @@ -115,7 +115,7 @@ func (m *Module) threadGETHandler(c *gin.Context) { } // Prepare stylesheets for thread. - stylesheets := make([]string, 0, 5) + stylesheets := make([]string, 0, 6) // Basic thread stylesheets. stylesheets = append( @@ -131,6 +131,7 @@ func (m *Module) threadGETHandler(c *gin.Context) { if theme := targetAccount.Theme; theme != "" { stylesheets = append( stylesheets, + instanceCustomCSSPath, themesPathPrefix+"/"+theme, ) } diff --git a/internal/web/web.go b/internal/web/web.go index 185bf7120..35f8f21b0 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -36,20 +36,21 @@ import ( ) const ( - confirmEmailPath = "/" + uris.ConfirmEmailPath - profileGroupPath = "/@:username" - statusPath = "/statuses/:" + apiutil.WebStatusIDKey // leave out the '/@:username' prefix as this will be served within the profile group - tagsPath = "/tags/:" + apiutil.TagNameKey - customCSSPath = profileGroupPath + "/custom.css" - rssFeedPath = profileGroupPath + "/feed.rss" - assetsPathPrefix = "/assets" - distPathPrefix = assetsPathPrefix + "/dist" - themesPathPrefix = assetsPathPrefix + "/themes" - settingsPathPrefix = "/settings" - settingsPanelGlob = settingsPathPrefix + "/*panel" - userPanelPath = settingsPathPrefix + "/user" - adminPanelPath = settingsPathPrefix + "/admin" - signupPath = "/signup" + confirmEmailPath = "/" + uris.ConfirmEmailPath + profileGroupPath = "/@:username" + statusPath = "/statuses/:" + apiutil.WebStatusIDKey // leave out the '/@:username' prefix as this will be served within the profile group + tagsPath = "/tags/:" + apiutil.TagNameKey + customCSSPath = profileGroupPath + "/custom.css" + instanceCustomCSSPath = "/custom.css" + rssFeedPath = profileGroupPath + "/feed.rss" + assetsPathPrefix = "/assets" + distPathPrefix = assetsPathPrefix + "/dist" + themesPathPrefix = assetsPathPrefix + "/themes" + settingsPathPrefix = "/settings" + settingsPanelGlob = settingsPathPrefix + "/*panel" + userPanelPath = settingsPathPrefix + "/user" + adminPanelPath = settingsPathPrefix + "/admin" + signupPath = "/signup" cacheControlHeader = "Cache-Control" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control cacheControlNoCache = "no-cache" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#response_directives @@ -114,6 +115,7 @@ func (m *Module) Route(r *router.Router, mi ...gin.HandlerFunc) { r.AttachHandler(http.MethodGet, settingsPathPrefix, m.SettingsPanelHandler) r.AttachHandler(http.MethodGet, settingsPanelGlob, m.SettingsPanelHandler) r.AttachHandler(http.MethodGet, customCSSPath, m.customCSSGETHandler) + r.AttachHandler(http.MethodGet, instanceCustomCSSPath, m.instanceCustomCSSGETHandler) r.AttachHandler(http.MethodGet, rssFeedPath, m.rssFeedGETHandler) r.AttachHandler(http.MethodGet, confirmEmailPath, m.confirmEmailGETHandler) r.AttachHandler(http.MethodPost, confirmEmailPath, m.confirmEmailPOSTHandler) diff --git a/web/source/settings/lib/types/instance.ts b/web/source/settings/lib/types/instance.ts index 11f75032c..9abdc6a96 100644 --- a/web/source/settings/lib/types/instance.ts +++ b/web/source/settings/lib/types/instance.ts @@ -25,6 +25,7 @@ export interface InstanceV1 { description_text?: string; short_description: string; short_description_text?: string; + custom_css: string; email: string; version: string; debug?: boolean; diff --git a/web/source/settings/views/admin/instance/settings.tsx b/web/source/settings/views/admin/instance/settings.tsx index c769b11ec..fd5ceb1ee 100644 --- a/web/source/settings/views/admin/instance/settings.tsx +++ b/web/source/settings/views/admin/instance/settings.tsx @@ -46,7 +46,7 @@ function InstanceSettingsForm({ data: instance }: InstanceSettingsFormProps) { const shortDescLimit = 500; const descLimit = 5000; const termsLimit = 5000; - + const form = { title: useTextInput("title", { source: instance, @@ -66,6 +66,10 @@ function InstanceSettingsForm({ data: instance }: InstanceSettingsFormProps) { valueSelector: (s: InstanceV1) => s.description_text, validator: (val: string) => val.length <= descLimit ? "" : `Instance description is ${val.length} characters; must be ${descLimit} characters or less` }), + customCSS: useTextInput("custom_css", { + source: instance, + valueSelector: (s: InstanceV1) => s.custom_css + }), terms: useTextInput("terms", { source: instance, // Select "raw" text version of parsed field for editing. @@ -191,7 +195,16 @@ function InstanceSettingsForm({ data: instance }: InstanceSettingsFormProps) { type="email" /> +